quantumkv commited on
Commit
993f15b
·
verified ·
1 Parent(s): 5c3849c

Hi Design Team,

Browse files

I need to provide extremely comprehensive structural specifications for our upcoming Claude AI chat interface. This document covers every functional element, user flow, and technical requirement—please treat this as your primary blueprint for what to design (not how it should look).
1.0 PROJECT ARCHITECTURE OVERVIEW
We're building a single-page chat application that operates entirely client-side. The app will be delivered as one HTML file containing embedded CSS and JavaScript, with zero external dependencies except the Puter.js script tag. Users access multiple Claude AI models without API keys, authentication, or backend servers.
Core User Journey:
Land on page → See empty chat with onboarding message
Select AI model → Choose from 5 Claude variants
Input prompt → Type or click example button
Choose interaction mode → Standard (instant) or Stream (real-time)
Receive response → View formatted message in conversation thread
Continue conversation → Build context-aware chat history
2.0 HEADER SECTION (Fixed Position)
2.1 Left Zone: Application Identity
App Name: "Claude AI Chat" (text, not logo)
Tagline/Subtitle: "Powered by Puter.js" (optional)
Function: Clicking app name resets conversation (clear history)
2.2 Right Zone: Model Selection
Component: Dropdown selector (default: "claude-sonnet-4-5")
Options:
claude-sonnet-4-5 (label: "Claude Sonnet 4.5")
claude-sonnet-4 (label: "Claude Sonnet 4")
claude-opus-4-1 (label: "Claude Opus 4.1")
claude-opus-4 (label: "Claude Opus 4")
claude-haiku-4-5 (label: "Claude Haiku 4.5")
Behavior:
Changing model mid-conversation appends system message: "Model changed to [Model Name]"
Dropdown disabled during active requests
On mobile: Convert to bottom sheet or full-screen selector
2.3 Status Indicator Area
Location: Top-right corner, overlaying content
States:
Hidden (default)
Success: Green bar, auto-dismiss after 3 seconds
Error: Red bar, auto-dismiss after 5 seconds, contains "Retry" button
Loading: Persistent spinner with text "Connecting..."
3.0 MAIN CHAT CONTAINER (Scrollable Region)
3.1 Message Thread Structure
Container: Flex column, reverse chronological order (newest at bottom)
Scroll Behavior: Auto-scroll to bottom on new messages, unless user manually scrolled up
Message Components (per message):
Header: Role label ("You" or "Assistant") + timestamp (optional)
Body: Text content with markdown support
Metadata: Character count (optional, expandable)
Message Types:
User Message: Right-aligned, contains only plain text
Assistant Message: Left-aligned, contains:
Plain text
Code blocks (monospaced, syntax highlighting)
Inline code (backticks)
Lists (bulleted/numbered)
Links (clickable, open new tab)
3.2 Typing Indicator
Trigger: Appears immediately after user sends message
Structure: Three animated dots + "Claude is typing..." text
Position: Above assistant message, disappears when response starts
Stream Mode: Hidden, response appears directly
3.3 Empty State
Condition: Zero messages in thread
Content: Welcome message from assistant explaining capabilities
Actions: Prompt user to select example or type message
3.4 Message History Management
Persistence: Store last 50 messages in localStorage
Page Refresh: Reload history from localStorage
Reset: "New Chat" button clears history and container
Overflow: Auto-prune messages older than 50, show "Load older messages" button
4.0 EXAMPLE PROMPTS SECTION (Dynamic)
4.1 Container
Location: Between header and chat container
Visibility: Always visible until first user message, then collapsible
Structure: Horizontal flex wrap of button elements
4.2 Button Specifications
Default Prompts:
"Explain quantum computing in simple terms"
"Write a detailed essay on AI impact"
"Write a short poem about coding"
"Generate a JavaScript function to sort an array"
Behavior: Clicking injects text into textarea and focuses input
Customization: Allow users to edit prompt before sending
Dynamic Addition: Future feature: users can add custom example prompts
4.3 Responsive Behavior
Desktop: 4 buttons in row
Tablet: 2x2 grid
Mobile: Vertical stack, full-width buttons
5.0 INPUT AREA (Fixed Bottom)
5.1 Text Area Component
Type: Multi-line textarea (not single-line input)
Auto-resize: Grows vertically up to 150px max-height
Placeholder: "Type your message here... (Shift+Enter for new line)"
Keyboard Shortcuts:
Enter → Send message
Shift + Enter → New line
Cmd/Ctrl + K → Clear input
Cmd/Ctrl + N → New chat
Character Limit: 10,000 characters (show counter when >80%)
Disabled States: During API requests, show "Waiting for response..."
Paste Behavior: Accept text, code, and markdown; strip images/files
5.2 Button Group
Primary Button (Left): "Send"
Sends standard request, waits for complete response
Disabled when textarea empty or during active request
Icon: Paper plane or arrow right
Secondary Button (Right): "Stream"
Sends streaming request, shows real-time text generation
Best for long responses (essays, code generation)
Disabled when textarea empty or during active request
Icon: Lightning bolt or pulse waves
Tertiary Button (Optional): "New Chat"
Resets conversation, clears history
Place in header or as floating action button
5.3 Input Validation
Empty Input: Disable buttons, show tooltip on click attempt
Rate Limiting: Queue messages if user spams, show "Please wait..." message
Network Error: If Puter.js fails to load, show persistent error bar: "Failed to connect to AI service. Refresh page."
6.0 INTERACTION FLOWS (Detailed)
6.1 Standard Message Flow
User types message → Clicks "Send"
Input locked, buttons disabled, typing indicator appears
API request sent via puter.ai.chat() (non-streaming)
On success:
Typing indicator hidden
Assistant message rendered in full
Message saved to history
Input unlocked
On error:
Show error status: "Request failed. [Error message]"
Input unlocked, message preserved for retry
6.2 Streaming Message Flow
User types message → Clicks "Stream"
Input locked, buttons disabled, empty assistant message appears
API request sent with {stream: true} parameter
Response chunks arrive → Append to message in real-time
On completion: Input unlocked, message saved
On error: Show error bar, preserve partial response with "Streaming interrupted"
6.3 Model Switching Flow
User opens dropdown → Selects new model
If conversation empty: Silent switch
If conversation active:
Append system message to chat: "Model changed to [New Model] — previous context retained"
Dropdown shows checkmark briefly
Selected model becomes default for new chats
6.4 Error Recovery Flow
Network Error: Show "Retry" button in error status, clicking resends last message
Rate Limit: Show "Too many requests. Wait 30 seconds." with countdown
Model Unavailable: Auto-suggest next best model, show prompt: "[Model] unavailable. Use [Alternative] instead?"
7.0 TECHNICAL INTEGRATION POINTS
7.1 Puter.js API Calls
Host Script: <script src="https://js.puter.com/v2/"></script> in <head>
Standard Call: puter.ai.chat(message, {model: selectedModel})
Streaming Call: puter.ai.chat(message, {model: selectedModel, stream: true})
Response Handling:
Standard: response.message.content[0].text
Streaming: Async iterator for await (const part of response)
7.2 State Management
Global Variables:
conversationHistory (array of message objects)
selectedModel (string)
isProcessing (boolean)
isStreaming (boolean)
localStorage Keys:
claudeChat_history (JSON string)
claudeChat_selectedModel (string)
claudeChat_timestamp (for cache invalidation)
7.3 Performance Requirements
First Paint: < 1 second
Time to Interactive: < 2 seconds (including Puter.js load)
Message Render: < 50ms for new message insertion
Stream Latency: < 100ms between chunks
Memory: < 50MB total footprint
8.0 RESPONSIVE BREAKPOINTS & ADAPTIVE BEHAVIOR
8.1 Desktop (≥1024px)
Three-column layout: Header, Examples, Chat, Input
Chat container max-width: 1200px, centered
Buttons side-by-side
Model selector as dropdown
8.2 Tablet (768px – 1023px)
Two-column layout: Header full-width, Examples 2x2 grid, Chat, Input
Chat container: 90% width
Buttons slightly larger touch targets (44px min)
8.3 Mobile (≤767px)
Single-column stacked layout
Header: App name and model selector on same line, smaller font
Examples: Vertical stack, full-width buttons
Chat: Full-width, 1rem padding
Input: Textarea + buttons in flex column, buttons full-width stacked
Mobile-Specific: Add floating "Scroll to bottom" button when user scrolls up
9.0 ACCESSIBILITY (WCAG 2.1 AA Compliance)
Keyboard Navigation: All interactive elements reachable via Tab
Screen Readers: ARIA labels for dynamic content (role="status" for messages)
Focus Management: Return focus to textarea after sending
Color Contrast: Minimum 4.5:1 ratio for all text
Motion: Respect prefers-reduced-motion (disable animations)
Alt Text: Icons must have aria-hidden or descriptive labels
10.0 FUTURE-PROOFING STRUCTURE
Design components to support upcoming features:
**File Upload:**预留 drag-drop zone above textarea
Multi-turn Conversations: Message threading indentation
User Accounts: Login button space in header
Settings Panel: Gear icon in header → modal/sheet
Export Chat: Download button per message or entire thread
Voice Input: Mic button next to textarea
Please design the interface structure based on these specifications. Focus on layout, component hierarchy, interaction patterns, and responsive behavior. Visual styling (colors, fonts, effects) is your creative domain—just ensure the underlying UX skeleton matches this document exactly.

Files changed (6) hide show
  1. README.md +8 -5
  2. components/header.js +162 -0
  3. components/status-bar.js +215 -0
  4. index.html +110 -19
  5. script.js +362 -0
  6. style.css +289 -18
README.md CHANGED
@@ -1,10 +1,13 @@
1
  ---
2
- title: Claude Chat Interface
3
- emoji: 📉
4
- colorFrom: gray
5
- colorTo: purple
6
  sdk: static
7
  pinned: false
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
1
  ---
2
+ title: Claude Chat Interface
3
+ colorFrom: pink
4
+ colorTo: green
5
+ emoji: 🐳
6
  sdk: static
7
  pinned: false
8
+ tags:
9
+ - deepsite-v3
10
  ---
11
 
12
+ # Welcome to your new DeepSite project!
13
+ This project was created with [DeepSite](https://huggingface.co/deepsite).
components/header.js ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class AppHeader extends HTMLElement {
2
+ constructor() {
3
+ super();
4
+ this.attachShadow({ mode: 'open' });
5
+ }
6
+
7
+ connectedCallback() {
8
+ this.render();
9
+ this.setupEventListeners();
10
+ }
11
+
12
+ render() {
13
+ this.shadowRoot.innerHTML = `
14
+ <style>
15
+ header {
16
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
17
+ color: white;
18
+ padding: 1rem 0;
19
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
20
+ }
21
+
22
+ .header-container {
23
+ max-width: 1200px;
24
+ margin: 0 auto;
25
+ padding: 0 1rem;
26
+ display: flex;
27
+ justify-content: space-between;
28
+ align-items: center;
29
+ }
30
+
31
+ .app-title {
32
+ cursor: pointer;
33
+ display: flex;
34
+ align-items: center;
35
+ gap: 0.5rem;
36
+ transition: transform 0.2s;
37
+ }
38
+
39
+ .app-title:hover {
40
+ transform: scale(1.02);
41
+ }
42
+
43
+ .app-title:active {
44
+ transform: scale(0.98);
45
+ }
46
+
47
+ .app-name {
48
+ font-size: 1.5rem;
49
+ font-weight: 700;
50
+ letter-spacing: -0.5px;
51
+ }
52
+
53
+ .app-subtitle {
54
+ font-size: 0.75rem;
55
+ opacity: 0.8;
56
+ margin-top: 0.125rem;
57
+ }
58
+
59
+ .model-selector {
60
+ background: rgba(255, 255, 255, 0.1);
61
+ border: 1px solid rgba(255, 255, 255, 0.2);
62
+ color: white;
63
+ padding: 0.5rem 1rem;
64
+ border-radius: 8px;
65
+ font-size: 0.875rem;
66
+ cursor: pointer;
67
+ transition: all 0.2s;
68
+ backdrop-filter: blur(10px);
69
+ }
70
+
71
+ .model-selector:hover {
72
+ background: rgba(255, 255, 255, 0.2);
73
+ border-color: rgba(255, 255, 255, 0.3);
74
+ }
75
+
76
+ .model-selector:focus {
77
+ outline: 2px solid rgba(255, 255, 255, 0.5);
78
+ outline-offset: 2px;
79
+ }
80
+
81
+ .model-selector:disabled {
82
+ opacity: 0.5;
83
+ cursor: not-allowed;
84
+ }
85
+
86
+ .model-selector option {
87
+ background: #4c51bf;
88
+ color: white;
89
+ }
90
+
91
+ @media (max-width: 640px) {
92
+ .header-container {
93
+ flex-direction: column;
94
+ gap: 1rem;
95
+ }
96
+
97
+ .app-name {
98
+ font-size: 1.25rem;
99
+ }
100
+
101
+ .model-selector {
102
+ width: 100%;
103
+ }
104
+ }
105
+ </style>
106
+
107
+ <header>
108
+ <div class="header-container">
109
+ <div class="app-title" role="button" aria-label="New chat">
110
+ <div>
111
+ <div class="app-name">Claude AI Chat</div>
112
+ <div class="app-subtitle">Powered by Puter.js</div>
113
+ </div>
114
+ </div>
115
+
116
+ <select class="model-selector" aria-label="Select AI model">
117
+ <option value="claude-sonnet-4-5">Claude Sonnet 4.5</option>
118
+ <option value="claude-sonnet-4">Claude Sonnet 4</option>
119
+ <option value="claude-opus-4-1">Claude Opus 4.1</option>
120
+ <option value="claude-opus-4">Claude Opus 4</option>
121
+ <option value="claude-haiku-4-5">Claude Haiku 4.5</option>
122
+ </select>
123
+ </div>
124
+ </header>
125
+ `;
126
+ }
127
+
128
+ setupEventListeners() {
129
+ // App title click - new chat
130
+ const appTitle = this.shadowRoot.querySelector('.app-title');
131
+ appTitle.addEventListener('click', () => {
132
+ if (window.claudeApp) {
133
+ window.claudeApp.newChat();
134
+ }
135
+ });
136
+
137
+ // Model selector change
138
+ const modelSelector = this.shadowRoot.querySelector('.model-selector');
139
+
140
+ // Set initial value
141
+ if (window.claudeApp) {
142
+ modelSelector.value = window.claudeApp.selectedModel;
143
+ }
144
+
145
+ modelSelector.addEventListener('change', (e) => {
146
+ if (window.claudeApp) {
147
+ window.claudeApp.changeModel(e.target.value);
148
+ }
149
+ });
150
+
151
+ // Update model selector when app state changes
152
+ if (window.claudeApp) {
153
+ const originalChangeModel = window.claudeApp.changeModel.bind(window.claudeApp);
154
+ window.claudeApp.changeModel = (newModel) => {
155
+ originalChangeModel(newModel);
156
+ modelSelector.value = newModel;
157
+ };
158
+ }
159
+ }
160
+ }
161
+
162
+ customElements.define('app-header', AppHeader);
components/status-bar.js ADDED
@@ -0,0 +1,215 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class StatusBar extends HTMLElement {
2
+ constructor() {
3
+ super();
4
+ this.attachShadow({ mode: 'open' });
5
+ this.currentTimeout = null;
6
+ }
7
+
8
+ connectedCallback() {
9
+ this.render();
10
+ }
11
+
12
+ render() {
13
+ this.shadowRoot.innerHTML = `
14
+ <style>
15
+ :host {
16
+ position: fixed;
17
+ top: 0;
18
+ left: 50%;
19
+ transform: translateX(-50%);
20
+ z-index: 1000;
21
+ pointer-events: none;
22
+ }
23
+
24
+ .status-bar {
25
+ min-width: 300px;
26
+ max-width: 600px;
27
+ padding: 0.75rem 1.5rem;
28
+ margin-top: 1rem;
29
+ border-radius: 8px;
30
+ display: flex;
31
+ align-items: center;
32
+ justify-content: space-between;
33
+ gap: 1rem;
34
+ font-size: 0.875rem;
35
+ font-weight: 500;
36
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
37
+ backdrop-filter: blur(10px);
38
+ opacity: 0;
39
+ transform: translateY(-20px);
40
+ transition: all 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
41
+ pointer-events: auto;
42
+ }
43
+
44
+ .status-bar.visible {
45
+ opacity: 1;
46
+ transform: translateY(0);
47
+ }
48
+
49
+ .status-bar.success {
50
+ background: linear-gradient(135deg, #10b981, #059669);
51
+ color: white;
52
+ }
53
+
54
+ .status-bar.error {
55
+ background: linear-gradient(135deg, #ef4444, #dc2626);
56
+ color: white;
57
+ }
58
+
59
+ .status-bar.loading {
60
+ background: linear-gradient(135deg, #3b82f6, #2563eb);
61
+ color: white;
62
+ }
63
+
64
+ .status-message {
65
+ flex: 1;
66
+ display: flex;
67
+ align-items: center;
68
+ gap: 0.5rem;
69
+ }
70
+
71
+ .status-icon {
72
+ flex-shrink: 0;
73
+ }
74
+
75
+ .status-action {
76
+ background: rgba(255, 255, 255, 0.2);
77
+ border: 1px solid rgba(255, 255, 255, 0.3);
78
+ color: white;
79
+ padding: 0.25rem 0.75rem;
80
+ border-radius: 4px;
81
+ font-size: 0.75rem;
82
+ cursor: pointer;
83
+ transition: all 0.2s;
84
+ }
85
+
86
+ .status-action:hover {
87
+ background: rgba(255, 255, 255, 0.3);
88
+ border-color: rgba(255, 255, 255, 0.4);
89
+ }
90
+
91
+ .loading-spinner {
92
+ width: 16px;
93
+ height: 16px;
94
+ border: 2px solid rgba(255, 255, 255, 0.3);
95
+ border-top-color: white;
96
+ border-radius: 50%;
97
+ animation: spin 0.8s linear infinite;
98
+ }
99
+
100
+ @keyframes spin {
101
+ to {
102
+ transform: rotate(360deg);
103
+ }
104
+ }
105
+
106
+ @media (max-width: 640px) {
107
+ :host {
108
+ left: 1rem;
109
+ right: 1rem;
110
+ transform: none;
111
+ }
112
+
113
+ .status-bar {
114
+ min-width: auto;
115
+ max-width: none;
116
+ }
117
+ }
118
+ </style>
119
+
120
+ <div id="statusBar" class="status-bar">
121
+ <div class="status-message">
122
+ <span class="status-icon"></span>
123
+ <span class="status-text"></span>
124
+ </div>
125
+ <button class="status-action hidden" id="statusAction"></button>
126
+ </div>
127
+ `;
128
+ }
129
+
130
+ show(type, message, duration = 3000, actionText = null, actionCallback = null) {
131
+ const statusBar = this.shadowRoot.getElementById('statusBar');
132
+ const statusText = this.shadowRoot.querySelector('.status-text');
133
+ const statusIcon = this.shadowRoot.querySelector('.status-icon');
134
+ const statusAction = this.shadowRoot.getElementById('statusAction');
135
+
136
+ // Clear any existing timeout
137
+ if (this.currentTimeout) {
138
+ clearTimeout(this.currentTimeout);
139
+ }
140
+
141
+ // Reset classes
142
+ statusBar.className = 'status-bar';
143
+
144
+ // Set type-specific content
145
+ switch (type) {
146
+ case 'success':
147
+ statusBar.classList.add('success');
148
+ statusIcon.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"></polyline></svg>';
149
+ break;
150
+
151
+ case 'error':
152
+ statusBar.classList.add('error');
153
+ statusIcon.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="15" y1="9" x2="9" y2="15"></line><line x1="9" y1="9" x2="15" y2="15"></line></svg>';
154
+ break;
155
+
156
+ case 'loading':
157
+ statusBar.classList.add('loading');
158
+ statusIcon.innerHTML = '<div class="loading-spinner"></div>';
159
+ break;
160
+ }
161
+
162
+ // Set message
163
+ statusText.textContent = message;
164
+
165
+ // Handle action button
166
+ if (actionText && actionCallback) {
167
+ statusAction.textContent = actionText;
168
+ statusAction.classList.remove('hidden');
169
+ statusAction.onclick = actionCallback;
170
+ } else {
171
+ statusAction.classList.add('hidden');
172
+ statusAction.onclick = null;
173
+ }
174
+
175
+ // Show status bar
176
+ setTimeout(() => {
177
+ statusBar.classList.add('visible');
178
+ }, 10);
179
+
180
+ // Auto-hide after duration (except for loading)
181
+ if (type !== 'loading' && duration > 0) {
182
+ this.currentTimeout = setTimeout(() => {
183
+ this.hide();
184
+ }, duration);
185
+ }
186
+ }
187
+
188
+ hide() {
189
+ const statusBar = this.shadowRoot.getElementById('statusBar');
190
+ statusBar.classList.remove('visible');
191
+
192
+ // Clear timeout
193
+ if (this.currentTimeout) {
194
+ clearTimeout(this.currentTimeout);
195
+ this.currentTimeout = null;
196
+ }
197
+ }
198
+
199
+ // Method to show success message
200
+ success(message, duration = 3000) {
201
+ this.show('success', message, duration);
202
+ }
203
+
204
+ // Method to show error message with retry
205
+ error(message, retryCallback = null) {
206
+ this.show('error', message, 5000, 'Retry', retryCallback);
207
+ }
208
+
209
+ // Method to show loading
210
+ loading(message = 'Connecting...') {
211
+ this.show('loading', message, 0);
212
+ }
213
+ }
214
+
215
+ customElements.define('status-bar', StatusBar);
index.html CHANGED
@@ -1,19 +1,110 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Claude AI Chat</title>
7
+ <link rel="stylesheet" href="style.css">
8
+ <script src="https://cdn.tailwindcss.com"></script>
9
+ <script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
10
+ <script src="https://unpkg.com/feather-icons"></script>
11
+ <script src="https://js.puter.com/v2/"></script>
12
+ <script src="components/header.js"></script>
13
+ <script src="components/status-bar.js"></script>
14
+ </head>
15
+ <body class="bg-gray-50 h-screen flex flex-col overflow-hidden">
16
+ <!-- Header Component -->
17
+ <app-header></app-header>
18
+
19
+ <!-- Status Indicator -->
20
+ <status-bar></status-bar>
21
+
22
+ <!-- Main Content Container -->
23
+ <main class="flex-1 flex flex-col max-w-7xl mx-auto w-full px-4 sm:px-6 lg:px-8 pt-4 pb-2">
24
+ <!-- Example Prompts Section -->
25
+ <section id="examplesSection" class="mb-4 transition-all duration-300">
26
+ <div class="flex flex-wrap gap-2 justify-center">
27
+ <button class="example-btn px-4 py-2.5 bg-white hover:bg-gray-100 text-gray-700 rounded-lg border border-gray-200 hover:border-gray-300 transition-all duration-200 text-sm font-medium shadow-sm hover:shadow-md transform hover:-translate-y-0.5" data-prompt="Explain quantum computing in simple terms">
28
+ <i data-feather="cpu" class="inline-block w-4 h-4 mr-2"></i>
29
+ Explain quantum computing
30
+ </button>
31
+ <button class="example-btn px-4 py-2.5 bg-white hover:bg-gray-100 text-gray-700 rounded-lg border border-gray-200 hover:border-gray-300 transition-all duration-200 text-sm font-medium shadow-sm hover:shadow-md transform hover:-translate-y-0.5" data-prompt="Write a detailed essay on AI impact">
32
+ <i data-feather="edit-3" class="inline-block w-4 h-4 mr-2"></i>
33
+ Essay on AI impact
34
+ </button>
35
+ <button class="example-btn px-4 py-2.5 bg-white hover:bg-gray-100 text-gray-700 rounded-lg border border-gray-200 hover:border-gray-300 transition-all duration-200 text-sm font-medium shadow-sm hover:shadow-md transform hover:-translate-y-0.5" data-prompt="Write a short poem about coding">
36
+ <i data-feather="feather" class="inline-block w-4 h-4 mr-2"></i>
37
+ Poem about coding
38
+ </button>
39
+ <button class="example-btn px-4 py-2.5 bg-white hover:bg-gray-100 text-gray-700 rounded-lg border border-gray-200 hover:border-gray-300 transition-all duration-200 text-sm font-medium shadow-sm hover:shadow-md transform hover:-translate-y-0.5" data-prompt="Generate a JavaScript function to sort an array">
40
+ <i data-feather="code" class="inline-block w-4 h-4 mr-2"></i>
41
+ JS array sorter
42
+ </button>
43
+ </div>
44
+ </section>
45
+
46
+ <!-- Chat Container -->
47
+ <section id="chatContainer" class="flex-1 overflow-y-auto bg-white rounded-xl shadow-sm border border-gray-200 mb-4 relative">
48
+ <div id="messageThread" class="p-4 space-y-4">
49
+ <!-- Messages will be inserted here -->
50
+ </div>
51
+
52
+ <!-- Typing Indicator -->
53
+ <div id="typingIndicator" class="hidden absolute bottom-4 left-4 flex items-center space-x-2 text-gray-500">
54
+ <div class="flex space-x-1">
55
+ <span class="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style="animation-delay: 0ms"></span>
56
+ <span class="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style="animation-delay: 150ms"></span>
57
+ <span class="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style="animation-delay: 300ms"></span>
58
+ </div>
59
+ <span class="text-sm">Claude is typing...</span>
60
+ </div>
61
+
62
+ <!-- Scroll to Bottom Button -->
63
+ <button id="scrollToBottom" class="hidden absolute bottom-4 right-4 p-3 bg-blue-600 text-white rounded-full shadow-lg hover:bg-blue-700 transition-all duration-200 transform hover:scale-110" aria-label="Scroll to bottom">
64
+ <i data-feather="chevron-down" class="w-5 h-5"></i>
65
+ </button>
66
+ </section>
67
+
68
+ <!-- Input Area -->
69
+ <section class="bg-white rounded-xl shadow-sm border border-gray-200 p-4">
70
+ <div class="relative">
71
+ <textarea
72
+ id="messageInput"
73
+ class="w-full resize-none rounded-lg border border-gray-300 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 transition-all duration-200 p-3 pr-24 text-gray-700 placeholder-gray-400"
74
+ placeholder="Type your message here... (Shift+Enter for new line)"
75
+ rows="1"
76
+ maxlength="10000"
77
+ ></textarea>
78
+
79
+ <div class="absolute bottom-3 right-3 flex space-x-2">
80
+ <button
81
+ id="sendBtn"
82
+ class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-all duration-200 flex items-center space-x-2 disabled:opacity-50 disabled:cursor-not-allowed transform hover:scale-105 active:scale-95"
83
+ aria-label="Send message"
84
+ >
85
+ <i data-feather="send" class="w-4 h-4"></i>
86
+ <span class="hidden sm:inline">Send</span>
87
+ </button>
88
+ <button
89
+ id="streamBtn"
90
+ class="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg transition-all duration-200 flex items-center space-x-2 disabled:opacity-50 disabled:cursor-not-allowed transform hover:scale-105 active:scale-95"
91
+ aria-label="Stream response"
92
+ >
93
+ <i data-feather="zap" class="w-4 h-4"></i>
94
+ <span class="hidden sm:inline">Stream</span>
95
+ </button>
96
+ </div>
97
+ </div>
98
+
99
+ <!-- Character Counter -->
100
+ <div id="charCounter" class="mt-2 text-xs text-gray-500 text-right hidden">
101
+ <span id="charCount">0</span> / 10000
102
+ </div>
103
+ </section>
104
+ </main>
105
+
106
+ <script src="script.js"></script>
107
+ <script>feather.replace();</script>
108
+ <script src="https://huggingface.co/deepsite/deepsite-badge.js"></script>
109
+ </body>
110
+ </html>
script.js ADDED
@@ -0,0 +1,362 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Claude AI Chat Application State Management
2
+ class ClaudeChatApp {
3
+ constructor() {
4
+ this.conversationHistory = [];
5
+ this.selectedModel = 'claude-sonnet-4-5';
6
+ this.isProcessing = false;
7
+ this.isStreaming = false;
8
+ this.messageQueue = [];
9
+ this.userScrolled = false;
10
+
11
+ this.initializeApp();
12
+ }
13
+
14
+ initializeApp() {
15
+ this.loadFromStorage();
16
+ this.setupEventListeners();
17
+ this.renderMessages();
18
+ this.updateUIState();
19
+
20
+ // Check if Puter.js loaded
21
+ if (typeof puter === 'undefined') {
22
+ this.showStatus('error', 'Failed to connect to AI service. Refresh page.', 0);
23
+ }
24
+ }
25
+
26
+ loadFromStorage() {
27
+ try {
28
+ const history = localStorage.getItem('claudeChat_history');
29
+ const model = localStorage.getItem('claudeChat_selectedModel');
30
+
31
+ if (history) {
32
+ this.conversationHistory = JSON.parse(history);
33
+ }
34
+ if (model) {
35
+ this.selectedModel = model;
36
+ }
37
+ } catch (error) {
38
+ console.error('Error loading from storage:', error);
39
+ }
40
+ }
41
+
42
+ saveToStorage() {
43
+ try {
44
+ localStorage.setItem('claudeChat_history', JSON.stringify(this.conversationHistory.slice(-50)));
45
+ localStorage.setItem('claudeChat_selectedModel', this.selectedModel);
46
+ } catch (error) {
47
+ console.error('Error saving to storage:', error);
48
+ }
49
+ }
50
+
51
+ setupEventListeners() {
52
+ // Message input events
53
+ const messageInput = document.getElementById('messageInput');
54
+ messageInput.addEventListener('input', () => this.handleInputChange());
55
+ messageInput.addEventListener('keydown', (e) => this.handleKeyPress(e));
56
+
57
+ // Button events
58
+ document.getElementById('sendBtn').addEventListener('click', () => this.sendMessage(false));
59
+ document.getElementById('streamBtn').addEventListener('click', () => this.sendMessage(true));
60
+
61
+ // Example prompt buttons
62
+ document.querySelectorAll('.example-btn').forEach(btn => {
63
+ btn.addEventListener('click', () => {
64
+ const prompt = btn.dataset.prompt;
65
+ messageInput.value = prompt;
66
+ messageInput.focus();
67
+ this.handleInputChange();
68
+
69
+ // Auto-hide examples after first click
70
+ if (this.conversationHistory.length === 0) {
71
+ setTimeout(() => this.hideExamples(), 500);
72
+ }
73
+ });
74
+ });
75
+
76
+ // Scroll to bottom button
77
+ const scrollBtn = document.getElementById('scrollToBottom');
78
+ scrollBtn.addEventListener('click', () => {
79
+ this.scrollToBottom();
80
+ scrollBtn.classList.add('hidden');
81
+ });
82
+
83
+ // Chat container scroll detection
84
+ const chatContainer = document.getElementById('chatContainer');
85
+ chatContainer.addEventListener('scroll', () => {
86
+ const isAtBottom = chatContainer.scrollTop + chatContainer.clientHeight >= chatContainer.scrollHeight - 50;
87
+ this.userScrolled = !isAtBottom;
88
+
89
+ if (!isAtBottom && this.conversationHistory.length > 2) {
90
+ scrollBtn.classList.remove('hidden');
91
+ } else {
92
+ scrollBtn.classList.add('hidden');
93
+ }
94
+ });
95
+
96
+ // Keyboard shortcuts
97
+ document.addEventListener('keydown', (e) => {
98
+ if (e.metaKey || e.ctrlKey) {
99
+ switch (e.key.toLowerCase()) {
100
+ case 'k':
101
+ e.preventDefault();
102
+ messageInput.value = '';
103
+ this.handleInputChange();
104
+ break;
105
+ case 'n':
106
+ e.preventDefault();
107
+ this.newChat();
108
+ break;
109
+ }
110
+ }
111
+ });
112
+ }
113
+
114
+ handleInputChange() {
115
+ const messageInput = document.getElementById('messageInput');
116
+ const charCount = messageInput.value.length;
117
+ const charCounter = document.getElementById('charCounter');
118
+ const charCountDisplay = document.getElementById('charCount');
119
+
120
+ // Update character counter
121
+ if (charCount > 0) {
122
+ charCounter.classList.remove('hidden');
123
+ charCountDisplay.textContent = charCount;
124
+
125
+ if (charCount > 8000) {
126
+ charCounter.classList.add('text-red-500');
127
+ } else {
128
+ charCounter.classList.remove('text-red-500');
129
+ }
130
+ } else {
131
+ charCounter.classList.add('hidden');
132
+ }
133
+
134
+ // Auto-resize textarea
135
+ messageInput.style.height = 'auto';
136
+ messageInput.style.height = Math.min(messageInput.scrollHeight, 150) + 'px';
137
+
138
+ this.updateUIState();
139
+ }
140
+
141
+ handleKeyPress(e) {
142
+ if (e.key === 'Enter' && !e.shiftKey) {
143
+ e.preventDefault();
144
+ if (!this.isProcessing) {
145
+ this.sendMessage(false);
146
+ }
147
+ }
148
+ }
149
+
150
+ updateUIState() {
151
+ const messageInput = document.getElementById('messageInput');
152
+ const sendBtn = document.getElementById('sendBtn');
153
+ const streamBtn = document.getElementById('streamBtn');
154
+ const hasText = messageInput.value.trim().length > 0;
155
+
156
+ sendBtn.disabled = !hasText || this.isProcessing;
157
+ streamBtn.disabled = !hasText || this.isProcessing;
158
+
159
+ // Update button labels when processing
160
+ if (this.isProcessing) {
161
+ sendBtn.innerHTML = '<i data-feather="loader" class="w-4 h-4 animate-spin"></i><span class="hidden sm:inline">Sending</span>';
162
+ streamBtn.innerHTML = '<i data-feather="loader" class="w-4 h-4 animate-spin"></i><span class="hidden sm:inline">Streaming</span>';
163
+ } else {
164
+ sendBtn.innerHTML = '<i data-feather="send" class="w-4 h-4"></i><span class="hidden sm:inline">Send</span>';
165
+ streamBtn.innerHTML = '<i data-feather="zap" class="w-4 h-4"></i><span class="hidden sm:inline">Stream</span>';
166
+ }
167
+
168
+ feather.replace();
169
+ }
170
+
171
+ async sendMessage(stream = false) {
172
+ const messageInput = document.getElementById('messageInput');
173
+ const message = messageInput.value.trim();
174
+
175
+ if (!message || this.isProcessing) return;
176
+
177
+ this.isProcessing = true;
178
+ this.isStreaming = stream;
179
+ this.updateUIState();
180
+ this.hideExamples();
181
+
182
+ // Add user message
183
+ this.addMessage('user', message);
184
+
185
+ // Clear input
186
+ messageInput.value = '';
187
+ this.handleInputChange();
188
+
189
+ // Show typing indicator or empty message
190
+ if (stream) {
191
+ this.addMessage('assistant', '', true);
192
+ } else {
193
+ document.getElementById('typingIndicator').classList.remove('hidden');
194
+ }
195
+
196
+ try {
197
+ if (typeof puter === 'undefined') {
198
+ throw new Error('AI service not available');
199
+ }
200
+
201
+ if (stream) {
202
+ await this.handleStreamResponse(message);
203
+ } else {
204
+ await this.handleStandardResponse(message);
205
+ }
206
+
207
+ this.showStatus('success', 'Response received', 3000);
208
+ } catch (error) {
209
+ console.error('Error sending message:', error);
210
+ this.showStatus('error', `Request failed: ${error.message}`, 5000);
211
+
212
+ if (stream) {
213
+ // Update streaming message with error
214
+ const lastMessage = this.conversationHistory[this.conversationHistory.length - 1];
215
+ lastMessage.content += '\n\n*Streaming interrupted due to an error*';
216
+ this.renderMessages();
217
+ } else {
218
+ document.getElementById('typingIndicator').classList.add('hidden');
219
+ }
220
+ } finally {
221
+ this.isProcessing = false;
222
+ this.isStreaming = false;
223
+ this.updateUIState();
224
+ this.saveToStorage();
225
+
226
+ if (!stream) {
227
+ document.getElementById('typingIndicator').classList.add('hidden');
228
+ }
229
+ }
230
+ }
231
+
232
+ async handleStandardResponse(message) {
233
+ const response = await puter.ai.chat(message, { model: this.selectedModel });
234
+
235
+ if (response && response.message && response.message.content && response.message.content[0]) {
236
+ const content = response.message.content[0].text;
237
+ this.addMessage('assistant', content);
238
+ } else {
239
+ throw new Error('Invalid response format');
240
+ }
241
+ }
242
+
243
+ async handleStreamResponse(message) {
244
+ const response = await puter.ai.chat(message, {
245
+ model: this.selectedModel,
246
+ stream: true
247
+ });
248
+
249
+ let fullContent = '';
250
+ const messageIndex = this.conversationHistory.length - 1;
251
+
252
+ try {
253
+ for await (const part of response) {
254
+ if (part && part.choices && part.choices[0] && part.choices[0].delta) {
255
+ const chunk = part.choices[0].delta.content || '';
256
+ fullContent += chunk;
257
+ this.conversationHistory[messageIndex].content = fullContent;
258
+ this.updateStreamingMessage(messageIndex, fullContent);
259
+ }
260
+ }
261
+ } catch (error) {
262
+ console.error('Stream error:', error);
263
+ throw error;
264
+ }
265
+ }
266
+
267
+ updateStreamingMessage(index, content) {
268
+ const messagesContainer = document.getElementById('messageThread');
269
+ const messageElement = messagesContainer.children[index];
270
+ const contentElement = messageElement.querySelector('.message-content');
271
+
272
+ if (contentElement) {
273
+ contentElement.innerHTML = this.parseMarkdown(content);
274
+ }
275
+
276
+ if (!this.userScrolled) {
277
+ this.scrollToBottom();
278
+ }
279
+ }
280
+
281
+ addMessage(role, content, isStreaming = false) {
282
+ const message = {
283
+ role,
284
+ content,
285
+ timestamp: new Date().toISOString(),
286
+ id: Date.now()
287
+ };
288
+
289
+ this.conversationHistory.push(message);
290
+
291
+ if (!isStreaming) {
292
+ this.renderMessages();
293
+ if (!this.userScrolled) {
294
+ this.scrollToBottom();
295
+ }
296
+ } else {
297
+ // Create empty message element for streaming
298
+ this.renderSingleMessage(this.conversationHistory.length - 1);
299
+ }
300
+ }
301
+
302
+ renderMessages() {
303
+ const messagesContainer = document.getElementById('messageThread');
304
+ messagesContainer.innerHTML = '';
305
+
306
+ // Show empty state if no messages
307
+ if (this.conversationHistory.length === 0) {
308
+ this.showEmptyState();
309
+ return;
310
+ }
311
+
312
+ // Render all messages
313
+ this.conversationHistory.forEach((message, index) => {
314
+ this.renderSingleMessage(index);
315
+ });
316
+ }
317
+
318
+ renderSingleMessage(index) {
319
+ const messagesContainer = document.getElementById('messageThread');
320
+ const message = this.conversationHistory[index];
321
+
322
+ const messageDiv = document.createElement('div');
323
+ messageDiv.className = `message ${message.role === 'system' ? 'message-system' : message.role === 'user' ? 'message-user' : 'message-assistant'} p-4`;
324
+ messageDiv.setAttribute('role', 'article');
325
+ messageDiv.setAttribute('aria-label', `${message.role === 'user' ? 'You' : 'Claude'} message`);
326
+
327
+ const headerDiv = document.createElement('div');
328
+ headerDiv.className = 'message-header mb-2';
329
+ headerDiv.textContent = message.role === 'user' ? 'You' : message.role === 'system' ? 'System' : 'Claude';
330
+
331
+ const contentDiv = document.createElement('div');
332
+ contentDiv.className = 'message-content';
333
+
334
+ if (message.content) {
335
+ contentDiv.innerHTML = this.parseMarkdown(message.content);
336
+ } else {
337
+ contentDiv.innerHTML = '<span class="text-gray-400">Thinking...</span>';
338
+ }
339
+
340
+ messageDiv.appendChild(headerDiv);
341
+ messageDiv.appendChild(contentDiv);
342
+ messagesContainer.appendChild(messageDiv);
343
+
344
+ // Add animation
345
+ requestAnimationFrame(() => {
346
+ messageDiv.style.opacity = '1';
347
+ });
348
+ }
349
+
350
+ parseMarkdown(text) {
351
+ // Simple markdown parser
352
+ return text
353
+ // Headers
354
+ .replace(/^### (.*$)/gim, '<h3>$1</h3>')
355
+ .replace(/^## (.*$)/gim, '<h2>$1</h2>')
356
+ .replace(/^# (.*$)/gim, '<h1>$1</h1>')
357
+ // Bold
358
+ .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
359
+ // Italic
360
+ .replace(/\*(.+?)\*/g, '<em>$1</em>')
361
+ // Code blocks
362
+ .replace(/
style.css CHANGED
@@ -1,28 +1,299 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  body {
2
- padding: 2rem;
3
- font-family: -apple-system, BlinkMacSystemFont, "Arial", sans-serif;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  }
5
 
6
- h1 {
7
- font-size: 16px;
8
- margin-top: 0;
 
 
 
9
  }
10
 
11
- p {
12
- color: rgb(107, 114, 128);
13
- font-size: 15px;
14
- margin-bottom: 10px;
15
- margin-top: 5px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  }
17
 
18
- .card {
19
- max-width: 620px;
20
- margin: 0 auto;
21
- padding: 16px;
22
- border: 1px solid lightgray;
23
- border-radius: 16px;
24
  }
25
 
26
- .card p:last-child {
27
- margin-bottom: 0;
28
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Custom styles for Claude Chat Interface */
2
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
3
+
4
+ :root {
5
+ --primary-color: #3B82F6;
6
+ --primary-hover: #2563EB;
7
+ --success-color: #10B981;
8
+ --error-color: #EF4444;
9
+ --warning-color: #F59E0B;
10
+ --text-primary: #1F2937;
11
+ --text-secondary: #6B7280;
12
+ --border-color: #E5E7EB;
13
+ --bg-secondary: #F9FAFB;
14
+ }
15
+
16
+ * {
17
+ font-family: 'Inter', system-ui, -apple-system, sans-serif;
18
+ }
19
+
20
  body {
21
+ overscroll-behavior: contain;
22
+ }
23
+
24
+ /* Message Styles */
25
+ .message {
26
+ animation: messageSlideIn 0.3s ease-out;
27
+ transition: all 0.2s ease;
28
+ }
29
+
30
+ @keyframes messageSlideIn {
31
+ from {
32
+ opacity: 0;
33
+ transform: translateY(10px);
34
+ }
35
+ to {
36
+ opacity: 1;
37
+ transform: translateY(0);
38
+ }
39
+ }
40
+
41
+ .message-user {
42
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
43
+ color: white;
44
+ margin-left: auto;
45
+ max-width: 70%;
46
+ border-radius: 18px 18px 4px 18px;
47
+ }
48
+
49
+ .message-assistant {
50
+ background: white;
51
+ border: 1px solid var(--border-color);
52
+ margin-right: auto;
53
+ max-width: 85%;
54
+ border-radius: 18px 18px 18px 4px;
55
+ }
56
+
57
+ .message-system {
58
+ background: var(--bg-secondary);
59
+ border-left: 3px solid var(--warning-color);
60
+ text-align: center;
61
+ max-width: 100%;
62
+ border-radius: 8px;
63
  }
64
 
65
+ .message-header {
66
+ font-size: 0.75rem;
67
+ font-weight: 600;
68
+ text-transform: uppercase;
69
+ letter-spacing: 0.5px;
70
+ opacity: 0.8;
71
  }
72
 
73
+ .message-content {
74
+ line-height: 1.6;
75
+ word-wrap: break-word;
76
+ }
77
+
78
+ /* Markdown Styles */
79
+ .message-content h1,
80
+ .message-content h2,
81
+ .message-content h3,
82
+ .message-content h4,
83
+ .message-content h5,
84
+ .message-content h6 {
85
+ font-weight: 600;
86
+ margin: 1rem 0 0.5rem;
87
+ }
88
+
89
+ .message-content h1 { font-size: 1.5rem; }
90
+ .message-content h2 { font-size: 1.25rem; }
91
+ .message-content h3 { font-size: 1.125rem; }
92
+
93
+ .message-content p {
94
+ margin: 0.75rem 0;
95
  }
96
 
97
+ .message-content ul,
98
+ .message-content ol {
99
+ margin: 0.75rem 0;
100
+ padding-left: 1.5rem;
 
 
101
  }
102
 
103
+ .message-content li {
104
+ margin: 0.25rem 0;
105
  }
106
+
107
+ .message-content code {
108
+ background: #f3f4f6;
109
+ padding: 0.125rem 0.375rem;
110
+ border-radius: 4px;
111
+ font-family: 'Courier New', monospace;
112
+ font-size: 0.875em;
113
+ color: #d97706;
114
+ }
115
+
116
+ .message-content pre {
117
+ background: #1f2937;
118
+ color: #f9fafb;
119
+ padding: 1rem;
120
+ border-radius: 8px;
121
+ overflow-x: auto;
122
+ margin: 1rem 0;
123
+ }
124
+
125
+ .message-content pre code {
126
+ background: none;
127
+ color: inherit;
128
+ padding: 0;
129
+ }
130
+
131
+ .message-content a {
132
+ color: var(--primary-color);
133
+ text-decoration: underline;
134
+ transition: color 0.2s;
135
+ }
136
+
137
+ .message-content a:hover {
138
+ color: var(--primary-hover);
139
+ }
140
+
141
+ /* Textarea Styles */
142
+ #messageInput {
143
+ min-height: 44px;
144
+ max-height: 150px;
145
+ scrollbar-width: thin;
146
+ scrollbar-color: #CBD5E1 transparent;
147
+ }
148
+
149
+ #messageInput::-webkit-scrollbar {
150
+ width: 6px;
151
+ }
152
+
153
+ #messageInput::-webkit-scrollbar-track {
154
+ background: transparent;
155
+ }
156
+
157
+ #messageInput::-webkit-scrollbar-thumb {
158
+ background-color: #CBD5E1;
159
+ border-radius: 3px;
160
+ }
161
+
162
+ #messageInput:focus {
163
+ outline: none;
164
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
165
+ }
166
+
167
+ /* Chat Container Scrollbar */
168
+ #chatContainer {
169
+ scrollbar-width: thin;
170
+ scrollbar-color: #CBD5E1 transparent;
171
+ }
172
+
173
+ #chatContainer::-webkit-scrollbar {
174
+ width: 8px;
175
+ }
176
+
177
+ #chatContainer::-webkit-scrollbar-track {
178
+ background: transparent;
179
+ }
180
+
181
+ #chatContainer::-webkit-scrollbar-thumb {
182
+ background-color: #CBD5E1;
183
+ border-radius: 4px;
184
+ }
185
+
186
+ #chatContainer::-webkit-scrollbar-thumb:hover {
187
+ background-color: #9CA3AF;
188
+ }
189
+
190
+ /* Button States */
191
+ .btn-primary:active:not(:disabled) {
192
+ transform: scale(0.95);
193
+ }
194
+
195
+ .btn-secondary:active:not(:disabled) {
196
+ transform: scale(0.95);
197
+ }
198
+
199
+ /* Mobile Responsive Adjustments */
200
+ @media (max-width: 767px) {
201
+ .message-user {
202
+ max-width: 85%;
203
+ }
204
+
205
+ .message-assistant {
206
+ max-width: 90%;
207
+ }
208
+
209
+ #examplesSection .flex-wrap {
210
+ flex-direction: column;
211
+ }
212
+
213
+ #examplesSection button {
214
+ width: 100%;
215
+ justify-content: center;
216
+ }
217
+ }
218
+
219
+ /* Accessibility */
220
+ @media (prefers-reduced-motion: reduce) {
221
+ * {
222
+ animation-duration: 0.01ms !important;
223
+ animation-iteration-count: 1 !important;
224
+ transition-duration: 0.01ms !important;
225
+ }
226
+ }
227
+
228
+ /* Focus Styles for Accessibility */
229
+ button:focus-visible,
230
+ textarea:focus-visible,
231
+ select:focus-visible {
232
+ outline: 2px solid var(--primary-color);
233
+ outline-offset: 2px;
234
+ }
235
+
236
+ /* Loading Animation */
237
+ .loading-dots {
238
+ display: inline-flex;
239
+ gap: 4px;
240
+ }
241
+
242
+ .loading-dots span {
243
+ width: 8px;
244
+ height: 8px;
245
+ border-radius: 50%;
246
+ background-color: currentColor;
247
+ animation: loadingBounce 1.4s infinite ease-in-out both;
248
+ }
249
+
250
+ .loading-dots span:nth-child(1) {
251
+ animation-delay: -0.32s;
252
+ }
253
+
254
+ .loading-dots span:nth-child(2) {
255
+ animation-delay: -0.16s;
256
+ }
257
+
258
+ @keyframes loadingBounce {
259
+ 0%, 80%, 100% {
260
+ transform: scale(0);
261
+ opacity: 0.5;
262
+ }
263
+ 40% {
264
+ transform: scale(1);
265
+ opacity: 1;
266
+ }
267
+ }
268
+
269
+ /* Tooltip */
270
+ .tooltip {
271
+ position: relative;
272
+ }
273
+
274
+ .tooltip::before {
275
+ content: attr(data-tooltip);
276
+ position: absolute;
277
+ bottom: 100%;
278
+ left: 50%;
279
+ transform: translateX(-50%);
280
+ background: #1f2937;
281
+ color: white;
282
+ padding: 0.5rem 0.75rem;
283
+ border-radius: 6px;
284
+ font-size: 0.875rem;
285
+ white-space: nowrap;
286
+ opacity: 0;
287
+ pointer-events: none;
288
+ transition: opacity 0.2s;
289
+ margin-bottom: 0.5rem;
290
+ }
291
+
292
+ .tooltip:hover::before {
293
+ opacity: 1;
294
+ }
295
+
296
+ /* Selection highlight */
297
+ ::selection {
298
+ background-color: rgba(59, 130, 246, 0.2);
299
+ }