Fix team feedback: prompt, copy button, auth, layout, spacing

- Remove contact references from system prompt, add language matching rule
- Add copy-to-clipboard button on assistant messages with iframe fallback
- Increase token lifetime to 24h/30d, add refresh queue, remove hard redirect
- Fix adaptive layout for iframe/standalone, pin input at bottom
- Fix CSS specificity conflict (8px→2px spacing), add markdown post-processing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
SamoilenkoVadym 2026-02-11 12:16:09 +00:00
parent 31afa84abe
commit 02bbf6012f
9 changed files with 170 additions and 28 deletions

View file

@ -35,7 +35,7 @@ def create_access_token(
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(hours=1)
expire = datetime.utcnow() + timedelta(hours=24)
to_encode.update({"exp": expire, "iat": datetime.utcnow()})
@ -60,7 +60,7 @@ def create_refresh_token(data: Dict) -> str:
"""
return create_access_token(
data,
expires_delta=timedelta(days=7)
expires_delta=timedelta(days=30)
)

View file

@ -151,7 +151,7 @@ class AuthService:
user_id=user.id,
access_token_hash=hash_token(access_token),
refresh_token_hash=hash_token(refresh_token),
expires_at=datetime.utcnow() + timedelta(hours=1),
expires_at=datetime.utcnow() + timedelta(hours=24),
ip_address=ip_address,
user_agent=user_agent,
is_active=True,
@ -170,7 +170,7 @@ class AuthService:
"access_token": access_token,
"refresh_token": refresh_token,
"token_type": "bearer",
"expires_in": 3600, # 1 hour
"expires_in": 86400, # 24 hours
"user": {
"id": str(user.id),
"email": user.email,
@ -231,7 +231,7 @@ class AuthService:
user_id=user.id,
access_token_hash=hash_token(access_token),
refresh_token_hash=hash_token(refresh_token),
expires_at=datetime.utcnow() + timedelta(hours=1),
expires_at=datetime.utcnow() + timedelta(hours=24),
ip_address=ip_address,
user_agent=user_agent,
is_active=True,
@ -246,7 +246,7 @@ class AuthService:
"access_token": access_token,
"refresh_token": refresh_token,
"token_type": "bearer",
"expires_in": 3600, # 1 hour
"expires_in": 86400, # 24 hours
"user": {
"id": str(user.id),
"email": user.email,

View file

@ -56,13 +56,16 @@ class OpenAIService:
Adapt your response style:
- For "how to" questions Provide full onboarding-style guidance with navigation and links
- For "what is" / "where is" questions Provide direct, precise answers with sources
- Always include relevant links, contact info, and resources regardless of question type
- Always include relevant links and resources regardless of question type
🌐 LANGUAGE RULE:
Always respond in the SAME language as the user's CURRENT message. If the user switches languages between messages, you MUST switch immediately. Do NOT continue in a previous language.
CRITICAL RULES - STRICTLY ENFORCE:
1. ONLY answer using EXACT information from the file_search results provided
2. If file_search returns NO results or IRRELEVANT results, respond:
"I don't have information about that in my knowledge base. Please contact HR directly at [contact from docs if available]."
"I don't have information about that in my knowledge base. Please try rephrasing your question or check with your team directly."
3. NEVER use general knowledge, common sense, or assumptions
4. NEVER answer questions outside Oliver Agency APAC operations scope
@ -80,8 +83,7 @@ RESPONSE QUALITY RULES (ONBOARDING FOCUS):
4. **Important Links:** ALL URLs, SharePoint links, system links (make them clickable)
5. **Alternative Methods:** Other ways to accomplish the task (if applicable)
6. **Tips for New Users:** Additional context, common mistakes to avoid
7. **Need Help?:** Contact information or next steps
8. **Sources:** All source documents at the end
7. **Sources:** All source documents at the end
ONBOARDING-STYLE DETAILS TO INCLUDE:
- ALL URLs and links from documents (SharePoint, external sites, dashboards)
@ -91,7 +93,7 @@ ONBOARDING-STYLE DETAILS TO INCLUDE:
- Access requirements (permissions, accounts needed)
- Visual cues: "Look for the [Name] button in the top right corner"
- If question has multiple parts, answer EACH part thoroughly with separate sections
- Include specific details: names, dates, numbers, procedures, contacts, login info
- Include specific details: names, dates, numbers, procedures, login info
- If documents contain procedures/steps, list ALL steps in order with exact navigation
- NEVER summarize or shorten information - provide FULL details as if training a new employee
- Include context: WHAT it is, WHY they need it, WHERE to find it, HOW to use it
@ -142,9 +144,6 @@ For comprehensive instructions on logging time, including how to view assets by
- Timesheet data is typically updated daily
- For historical data, use the date range selector in the Agency Time Dashboard
**Need Help?**
If you need specific access or encounter any issues, contact Operations with your account details and the specific dashboard you need access to.
**Sources:** Ops Database_OpEx Framework_COSMICC.docx, Ops Database_Zoho_Updated_010826.docx, Ops Database_General Operations_Updated_01062026.docx"
ONBOARDING-STYLE GUIDELINES:
@ -178,7 +177,7 @@ Treat EVERY user as a NEW EMPLOYEE on their FIRST DAY. They need:
- All links and resources (SharePoint, dashboards, external sites)
- Context and explanations (not just facts)
- Tips and common gotchas
- Who to contact for help
- Where to find more information
Remember: You are an ONBOARDING GUIDE. Be thorough, include ALL links and navigation details, explain like teaching someone new. COMPLETE, DETAILED, WELL-STRUCTURED answers with ALL information from documents."""

View file

@ -15,11 +15,13 @@ const ChatInterface: React.FC = () => {
currentConversation,
messages,
isSending,
error,
sendMessage,
createConversation,
} = useChat();
const [messageText, setMessageText] = useState('');
const [copiedMessageId, setCopiedMessageId] = useState<string | null>(null);
const messagesEndRef = useRef<HTMLDivElement>(null);
const textareaRef = useRef<HTMLTextAreaElement>(null);
@ -36,6 +38,43 @@ const ChatInterface: React.FC = () => {
}
}, [messageText]);
const handleCopyMessage = async (messageId: string, content: string) => {
try {
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(content);
} else {
// Fallback for iframe/non-secure contexts (SharePoint)
const textArea = document.createElement('textarea');
textArea.value = content;
textArea.style.position = 'fixed';
textArea.style.left = '-9999px';
textArea.style.top = '-9999px';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
}
setCopiedMessageId(messageId);
setTimeout(() => setCopiedMessageId(null), 2000);
} catch (err) {
console.error('Failed to copy message:', err);
}
};
const cleanMarkdown = (text: string): string => {
return text
// Collapse 3+ empty lines into 2
.replace(/\n{3,}/g, '\n\n')
// Remove empty lines between heading and content
.replace(/(^#{1,6}\s+.+)\n{2,}(?=\S)/gm, '$1\n')
// Remove empty lines between bold headers and content
.replace(/(\*\*[^*]+\*\*:?.*)\n{2,}(?=\S)/gm, '$1\n')
// Remove empty lines between list items
.replace(/(\n[-*]\s+.+)\n{2,}(?=[-*]\s)/g, '$1\n')
.replace(/(\n\d+\.\s+.+)\n{2,}(?=\d+\.\s)/g, '$1\n');
};
const handleSend = async () => {
if (!messageText.trim() || isSending) return;
@ -95,6 +134,15 @@ const ChatInterface: React.FC = () => {
{message.role === 'user' ? '👤' : '🤖'}
</div>
<div className="message-content">
{message.role === 'assistant' && (
<button
className="btn-copy-message"
onClick={() => handleCopyMessage(message.id, message.content)}
title="Copy message"
>
{copiedMessageId === message.id ? '✓' : '📋'}
</button>
)}
<ReactMarkdown
components={{
// Custom paragraph styling
@ -149,7 +197,7 @@ const ChatInterface: React.FC = () => {
em: ({children}) => <em className="message-italic">{children}</em>,
}}
>
{message.content}
{message.role === 'assistant' ? cleanMarkdown(message.content) : message.content}
</ReactMarkdown>
{/* Display file search sources for assistant messages */}
@ -184,6 +232,12 @@ const ChatInterface: React.FC = () => {
</div>
</div>
)}
{error && !isSending && (
<div className="message-error-banner">
<span className="error-icon"></span>
<span>{error}</span>
</div>
)}
<div ref={messagesEndRef} />
</>
)}

View file

@ -32,6 +32,24 @@ apiClient.interceptors.request.use(
}
);
// Token refresh queue to prevent race conditions
let isRefreshing = false;
let failedQueue: Array<{
resolve: (value: any) => void;
reject: (reason?: any) => void;
}> = [];
const processQueue = (error: any, token: string | null = null) => {
failedQueue.forEach((promise) => {
if (error) {
promise.reject(error);
} else {
promise.resolve(token);
}
});
failedQueue = [];
};
// Response interceptor for error handling
apiClient.interceptors.response.use(
(response) => response,
@ -40,7 +58,18 @@ apiClient.interceptors.response.use(
// Handle 401 errors (token expired)
if (error.response?.status === 401 && !originalRequest._retry) {
if (isRefreshing) {
// Queue this request while refresh is in progress
return new Promise((resolve, reject) => {
failedQueue.push({ resolve, reject });
}).then((token) => {
originalRequest.headers.Authorization = `Bearer ${token}`;
return apiClient(originalRequest);
});
}
originalRequest._retry = true;
isRefreshing = true;
try {
const refreshToken = localStorage.getItem('refresh_token');
@ -53,16 +82,25 @@ apiClient.interceptors.response.use(
const { access_token } = response.data;
localStorage.setItem('access_token', access_token);
processQueue(null, access_token);
// Retry original request with new token
originalRequest.headers.Authorization = `Bearer ${access_token}`;
return apiClient(originalRequest);
}
} catch (refreshError) {
// Refresh failed, clear tokens and redirect to login
// No refresh token available
processQueue(error, null);
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
return Promise.reject(error);
} catch (refreshError) {
processQueue(refreshError, null);
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
window.location.href = '/';
return Promise.reject(refreshError);
} finally {
isRefreshing = false;
}
}

View file

@ -161,7 +161,7 @@
}
.message-content p {
margin: 0 0 var(--spacing-sm) 0;
margin: 0 0 2px 0;
}
.message-content p:last-child {
@ -170,7 +170,7 @@
.message-content ul,
.message-content ol {
margin: var(--spacing-sm) 0;
margin: 2px 0;
padding-left: var(--spacing-xl);
}

View file

@ -11,6 +11,7 @@
html, body, #root {
height: 100%;
width: 100%;
overflow: hidden;
}
body {

View file

@ -8,7 +8,7 @@
.app-container {
display: flex;
flex-direction: column;
height: 100vh;
height: 100%;
overflow: hidden;
}

View file

@ -76,11 +76,11 @@
body {
font-family: var(--font-family);
background: linear-gradient(135deg, var(--dark-primary) 0%, var(--dark-secondary) 100%);
min-height: 100vh;
min-height: 100%;
display: flex;
justify-content: center;
align-items: center;
padding: var(--spacing-xl);
align-items: stretch;
padding: 0;
color: var(--text-primary);
}
@ -188,6 +188,7 @@ body {
/* ========== CHAT BODY ========== */
.chat-body {
flex: 1;
min-height: 0;
padding: var(--spacing-xl);
overflow-y: auto;
display: flex;
@ -223,16 +224,15 @@ body {
/* ========== INPUT AREA ========== */
.chat-input {
padding: var(--spacing-lg) var(--spacing-xl);
padding: var(--spacing-lg) var(--spacing-xl) var(--spacing-xl);
background: var(--white);
border-top: 1px solid #e5e7eb;
display: flex;
gap: var(--spacing-md);
align-items: flex-end;
max-width: 900px;
margin: 0 auto;
width: 100%;
box-sizing: border-box;
flex-shrink: 0;
}
.input-wrapper {
@ -534,6 +534,7 @@ body {
.message-content {
flex: 1;
min-width: 0;
position: relative;
background: var(--white);
padding: var(--spacing-md) var(--spacing-lg);
border-radius: var(--radius-lg);
@ -775,6 +776,55 @@ body {
}
}
/* Hide empty paragraphs from markdown */
.message-content p:empty {
display: none;
}
/* Copy message button */
.btn-copy-message {
position: absolute;
top: 8px;
right: 8px;
opacity: 0;
background: var(--white);
border: 1px solid #e5e7eb;
border-radius: var(--radius-sm);
padding: 4px 8px;
font-size: 14px;
cursor: pointer;
transition: all var(--transition-fast);
z-index: 1;
line-height: 1;
}
.btn-copy-message:hover {
background: #f3f4f6;
border-color: var(--primary-gold);
}
.message-assistant:hover .btn-copy-message {
opacity: 1;
}
/* Error banner in chat */
.message-error-banner {
display: flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-md) var(--spacing-lg);
background: #fef2f2;
border: 1px solid #fecaca;
border-radius: var(--radius-md);
color: var(--error-red);
font-size: var(--font-size-sm);
animation: slideIn 0.3s ease-out;
}
.message-error-banner .error-icon {
flex-shrink: 0;
}
/* Ultra compact spacing for better structure */
.message-content p + p {
margin-top: 2px; /* Ultra minimal space between paragraphs */