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:
parent
31afa84abe
commit
02bbf6012f
9 changed files with 170 additions and 28 deletions
|
|
@ -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)
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
html, body, #root {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
.app-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue