lvwerra HF Staff commited on
Commit
1e3c7a4
·
0 Parent(s):
Files changed (9) hide show
  1. .gitignore +1 -0
  2. README.md +9 -0
  3. backend/.env.example +4 -0
  4. backend/README.md +134 -0
  5. backend/main.py +259 -0
  6. backend/requirements.txt +3 -0
  7. index.html +119 -0
  8. script.js +622 -0
  9. style.css +654 -0
.gitignore ADDED
@@ -0,0 +1 @@
 
 
1
+ settings.json
README.md ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ # Productive
2
+
3
+ Productive is a new way to interact with LLM assistants in a productive way. It works similar to JupyterLab with several tabs where each tab corresponds to a chat around a topic, a deep research query or a coding/data analysis session.
4
+
5
+ Types of notebooks:
6
+ - command center: this is where you land and launch new notebooks or agent workflows
7
+ - data analysis notebook: essentially a jupyter notebook with agents
8
+ - deep research: performing a research task with agents
9
+ - normal chat interface
backend/.env.example ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ # Optional: Hugging Face Token
2
+ # If you use huggingface.co endpoints without providing a token in settings,
3
+ # the backend will automatically use this environment variable as fallback
4
+ HF_TOKEN=your_hf_token_here
backend/README.md ADDED
@@ -0,0 +1,134 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Productive Backend
2
+
3
+ Minimal FastAPI backend for Productive with streaming support and action tokens.
4
+
5
+ ## Features
6
+
7
+ - **Streaming responses**: Tokens streamed in real-time via Server-Sent Events (SSE)
8
+ - **OpenAI-compatible**: Works with any OpenAI API-compatible endpoint
9
+ - **Action tokens**: LLM can suggest opening specialized notebooks via `[ACTION:TYPE]` tokens
10
+ - **CORS enabled**: Ready for frontend connection
11
+
12
+ ## Setup
13
+
14
+ 1. **Install dependencies**:
15
+ ```bash
16
+ cd backend
17
+ pip install -r requirements.txt
18
+ ```
19
+
20
+ 2. **Configure environment**:
21
+ ```bash
22
+ cp .env.example .env
23
+ ```
24
+
25
+ Edit `.env` and set your API credentials:
26
+ ```
27
+ OPENAI_API_KEY=your_api_key_here
28
+ OPENAI_BASE_URL=https://api.openai.com/v1
29
+ MODEL_NAME=gpt-4
30
+ ```
31
+
32
+ For other OpenAI-compatible providers (Anthropic via proxy, local models, etc.):
33
+ ```
34
+ OPENAI_BASE_URL=http://localhost:11434/v1 # Example: Ollama
35
+ MODEL_NAME=llama2
36
+ ```
37
+
38
+ 3. **Run the server**:
39
+ ```bash
40
+ python main.py
41
+ ```
42
+
43
+ Or with uvicorn directly:
44
+ ```bash
45
+ uvicorn main:app --reload --host 0.0.0.0 --port 8000
46
+ ```
47
+
48
+ 4. **Update frontend settings**:
49
+ - Open the frontend in browser
50
+ - Click SETTINGS
51
+ - Set endpoint to: `http://localhost:8000/api`
52
+ - Save
53
+
54
+ ## API Endpoints
55
+
56
+ ### `POST /api/chat/stream`
57
+
58
+ Stream chat responses with optional action tokens.
59
+
60
+ **Request**:
61
+ ```json
62
+ {
63
+ "messages": [
64
+ {"role": "user", "content": "Help me analyze data"}
65
+ ],
66
+ "notebook_type": "command",
67
+ "stream": true
68
+ }
69
+ ```
70
+
71
+ **Response**: Server-Sent Events stream
72
+ ```
73
+ data: {"type": "content", "content": "I"}
74
+ data: {"type": "content", "content": " can"}
75
+ data: {"type": "content", "content": " help"}
76
+ ...
77
+ data: {"type": "done"}
78
+ ```
79
+
80
+ ### Action Tokens
81
+
82
+ The LLM can suggest opening specialized notebooks by including action tokens:
83
+
84
+ - `[ACTION:AGENT]` - Open Agent notebook for multi-step tasks
85
+ - `[ACTION:CODE]` - Open Code notebook for data analysis/coding
86
+ - `[ACTION:RESEARCH]` - Open Research notebook for research tasks
87
+ - `[ACTION:CHAT]` - Open Chat notebook for continued conversation
88
+
89
+ The frontend automatically:
90
+ 1. Detects action tokens in the response
91
+ 2. Removes them from display
92
+ 3. Opens the appropriate notebook
93
+
94
+ ### `GET /health`
95
+
96
+ Health check endpoint.
97
+
98
+ ## Architecture
99
+
100
+ ```
101
+ Frontend (JavaScript/SSE)
102
+
103
+ FastAPI (streaming endpoint)
104
+
105
+ OpenAI API (or compatible)
106
+ ```
107
+
108
+ ## Testing
109
+
110
+ Test the endpoint directly:
111
+ ```bash
112
+ curl -X POST http://localhost:8000/api/chat/stream \
113
+ -H "Content-Type: application/json" \
114
+ -d '{
115
+ "messages": [{"role": "user", "content": "Hello"}],
116
+ "notebook_type": "command"
117
+ }'
118
+ ```
119
+
120
+ ## Troubleshooting
121
+
122
+ **"Connection error" in frontend**:
123
+ - Check backend is running: `http://localhost:8000`
124
+ - Verify endpoint in frontend settings
125
+ - Check browser console for CORS errors
126
+
127
+ **No streaming**:
128
+ - Ensure `stream: true` in request
129
+ - Check OpenAI API key is valid
130
+ - Verify model name is correct
131
+
132
+ **Action tokens not working**:
133
+ - Check LLM response includes `[ACTION:TYPE]` format
134
+ - View browser console for detection logs
backend/main.py ADDED
@@ -0,0 +1,259 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, HTTPException
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ from fastapi.responses import StreamingResponse
4
+ from pydantic import BaseModel
5
+ from typing import List, Optional
6
+ import json
7
+ import httpx
8
+ import os
9
+
10
+ app = FastAPI(title="Productive API")
11
+
12
+ # CORS middleware for frontend connection
13
+ app.add_middleware(
14
+ CORSMiddleware,
15
+ allow_origins=["*"], # In production, specify your frontend URL
16
+ allow_credentials=True,
17
+ allow_methods=["*"],
18
+ allow_headers=["*"],
19
+ )
20
+
21
+ # System prompts for each notebook type
22
+ SYSTEM_PROMPTS = {
23
+ "command": """You are a helpful AI assistant in the Productive interface command center.
24
+
25
+ When the user asks you to perform tasks that would benefit from a specialized notebook, you can suggest opening one by including an action tag at the END of your response.
26
+
27
+ Action tag format: <action:type>message for the new notebook</action>
28
+
29
+ Available action types:
30
+ - agent: For autonomous agent tasks that need multiple steps
31
+ - code: For coding, data analysis, or running code
32
+ - research: For research tasks requiring web search or deep analysis
33
+ - chat: For continuing general conversation
34
+
35
+ The message inside the action tag should be a clear prompt/instruction for the specialized notebook.
36
+
37
+ Examples:
38
+ - User: "Can you help me analyze this CSV file?"
39
+ Response: "I'll help you with that! <action:code>Analyze the CSV file and show statistics</action>"
40
+
41
+ - User: "Research the latest developments in AI"
42
+ Response: "I'll set that up for you. <action:research>Research latest AI developments in 2025</action>"
43
+
44
+ - User: "Create a multi-step workflow to organize my tasks"
45
+ Response: "Let me help with that! <action:agent>Create a task organization workflow with multiple steps</action>"
46
+
47
+ - User: "What's the weather like?" → No action tag needed
48
+
49
+ Only include ONE action tag at the very end if appropriate. The action tag will be removed from display and used to open a specialized notebook with the message as the initial prompt. Make sure the message passed along contains all necessary context.
50
+
51
+ Do not duplicate the answer: either answer the message directly OR use a dedicated notebook.
52
+ """,
53
+ "agent": """You are an autonomous agent assistant specialized in breaking down and executing multi-step tasks.
54
+
55
+ Your role is to:
56
+ - Understand complex tasks and break them down into clear steps
57
+ - Execute tasks methodically
58
+ - Keep track of progress and next steps
59
+ - Provide clear status updates
60
+
61
+ Focus on being proactive, organized, and thorough in completing multi-step workflows.
62
+ """,
63
+ "code": """You are a coding and data analysis assistant.
64
+
65
+ Your role is to:
66
+ - Help write, debug, and explain code
67
+ - Analyze data and provide insights
68
+ - Suggest best practices and optimizations
69
+ - Provide code examples with clear explanations
70
+
71
+ Focus on being precise, technical, and practical in your coding assistance.
72
+ """,
73
+ "research": """You are a research assistant specialized in deep analysis and information gathering.
74
+
75
+ Your role is to:
76
+ - Conduct thorough research on topics
77
+ - Synthesize information from multiple sources
78
+ - Provide well-structured, evidence-based answers
79
+ - Identify key insights and trends
80
+
81
+ Focus on being comprehensive, analytical, and well-sourced in your research.
82
+ """,
83
+ "chat": """You are a conversational AI assistant.
84
+
85
+ Your role is to:
86
+ - Engage in natural, helpful conversation
87
+ - Answer questions clearly and concisely
88
+ - Provide thoughtful responses
89
+ - Be friendly and approachable
90
+
91
+ Focus on being conversational, helpful, and easy to understand.
92
+ """
93
+ }
94
+
95
+
96
+ class Message(BaseModel):
97
+ role: str
98
+ content: str
99
+
100
+
101
+ class ChatRequest(BaseModel):
102
+ messages: List[Message]
103
+ notebook_type: str = "command"
104
+ stream: bool = True
105
+ endpoint: str # User's configured LLM endpoint
106
+ token: Optional[str] = None # Optional auth token
107
+ model: Optional[str] = "gpt-4" # Model name
108
+
109
+
110
+ async def stream_chat_response(
111
+ messages: List[dict],
112
+ endpoint: str,
113
+ token: Optional[str],
114
+ model: str,
115
+ notebook_type: str
116
+ ):
117
+ """Proxy stream from user's configured LLM endpoint"""
118
+
119
+ try:
120
+ print(f"=== Stream Request ===")
121
+ print(f"Endpoint: {endpoint}")
122
+ print(f"Model: {model}")
123
+ print(f"Messages: {len(messages)} messages")
124
+ print(f"Token provided: {bool(token)}")
125
+
126
+ # Prepare messages with appropriate system prompt based on notebook type
127
+ system_prompt = SYSTEM_PROMPTS.get(notebook_type, SYSTEM_PROMPTS["command"])
128
+ full_messages = [
129
+ {"role": "system", "content": system_prompt}
130
+ ] + messages
131
+
132
+ # Handle Hugging Face endpoint with fallback to HF_TOKEN
133
+ if not token and "huggingface.co" in endpoint:
134
+ token = os.getenv("HF_TOKEN")
135
+ if token:
136
+ print(f"Using HF_TOKEN from environment for Hugging Face endpoint")
137
+ else:
138
+ print(f"WARNING: No token provided and HF_TOKEN not found in environment!")
139
+
140
+ # Prepare headers
141
+ headers = {
142
+ "Content-Type": "application/json"
143
+ }
144
+ if token:
145
+ headers["Authorization"] = f"Bearer {token}"
146
+
147
+ # Prepare request body (OpenAI-compatible format)
148
+ request_body = {
149
+ "model": model,
150
+ "messages": full_messages,
151
+ "stream": True,
152
+ "temperature": 0.7
153
+ }
154
+
155
+ print(f"Sending request to: {endpoint}/chat/completions")
156
+
157
+ # Make streaming request to user's endpoint
158
+ async with httpx.AsyncClient(timeout=60.0) as client:
159
+ async with client.stream(
160
+ "POST",
161
+ f"{endpoint}/chat/completions",
162
+ json=request_body,
163
+ headers=headers
164
+ ) as response:
165
+ if response.status_code != 200:
166
+ error_text = await response.aread()
167
+ error_detail = error_text.decode() if error_text else f"Status {response.status_code}"
168
+ error_message = f"LLM API error ({response.status_code}): {error_detail}"
169
+ print(f"Error from LLM API: {error_message}")
170
+ yield f"data: {json.dumps({'type': 'error', 'content': error_message})}\n\n"
171
+ return
172
+
173
+ # Stream the response
174
+ async for line in response.aiter_lines():
175
+ if line.startswith("data: "):
176
+ data_str = line[6:] # Remove "data: " prefix
177
+
178
+ if data_str.strip() == "[DONE]":
179
+ yield f"data: {json.dumps({'type': 'done'})}\n\n"
180
+ continue
181
+
182
+ try:
183
+ data = json.loads(data_str)
184
+ # Extract content from OpenAI-compatible response
185
+ if "choices" in data and len(data["choices"]) > 0:
186
+ delta = data["choices"][0].get("delta", {})
187
+ content = delta.get("content")
188
+
189
+ if content:
190
+ # Forward the content token
191
+ yield f"data: {json.dumps({'type': 'content', 'content': content})}\n\n"
192
+ except json.JSONDecodeError:
193
+ # Skip malformed JSON
194
+ continue
195
+
196
+ except httpx.RequestError as e:
197
+ error_message = f"Connection error to LLM endpoint: {str(e)}"
198
+ print(f"HTTP Request Error: {e}")
199
+ yield f"data: {json.dumps({'type': 'error', 'content': error_message})}\n\n"
200
+ except Exception as e:
201
+ import traceback
202
+ error_message = f"Error: {str(e) or 'Unknown error occurred'}"
203
+ print(f"Exception in stream_chat_response: {e}")
204
+ print(traceback.format_exc())
205
+ yield f"data: {json.dumps({'type': 'error', 'content': error_message})}\n\n"
206
+
207
+
208
+ @app.get("/")
209
+ async def root():
210
+ return {
211
+ "message": "Productive API - LLM Proxy Server",
212
+ "version": "1.0.0",
213
+ "endpoints": {
214
+ "/api/chat/stream": "POST - Proxy streaming chat to user's LLM endpoint"
215
+ }
216
+ }
217
+
218
+
219
+ @app.post("/api/chat/stream")
220
+ async def chat_stream(request: ChatRequest):
221
+ """Proxy streaming chat to user's configured LLM endpoint"""
222
+
223
+ print("REQUEST", request.json)
224
+
225
+ if not request.messages:
226
+ raise HTTPException(status_code=400, detail="Messages are required")
227
+
228
+ if not request.endpoint:
229
+ raise HTTPException(status_code=400, detail="LLM endpoint is required")
230
+
231
+ # Convert Pydantic models to dicts
232
+ messages = [{"role": msg.role, "content": msg.content} for msg in request.messages]
233
+
234
+ return StreamingResponse(
235
+ stream_chat_response(
236
+ messages,
237
+ request.endpoint,
238
+ request.token,
239
+ request.model or "gpt-4",
240
+ request.notebook_type
241
+ ),
242
+ media_type="text/event-stream",
243
+ headers={
244
+ "Cache-Control": "no-cache",
245
+ "Connection": "keep-alive",
246
+ "X-Accel-Buffering": "no", # Disable nginx buffering
247
+ }
248
+ )
249
+
250
+
251
+ @app.get("/health")
252
+ async def health():
253
+ """Health check endpoint"""
254
+ return {"status": "healthy"}
255
+
256
+
257
+ if __name__ == "__main__":
258
+ import uvicorn
259
+ uvicorn.run(app, host="0.0.0.0", port=8000)
backend/requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ fastapi==0.104.1
2
+ uvicorn==0.24.0
3
+ httpx==0.25.0
index.html ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>Productive</title>
7
+ <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;700&display=swap" rel="stylesheet">
8
+ <link rel="stylesheet" href="style.css">
9
+ </head>
10
+ <body>
11
+ <div class="app-container">
12
+ <!-- Tab Bar -->
13
+ <div class="tab-bar">
14
+ <div class="tab active" data-tab-id="0">
15
+ <span class="tab-title">COMMAND</span>
16
+ </div>
17
+ <div id="dynamicTabs"></div>
18
+ <div class="new-tab-wrapper">
19
+ <button class="new-tab-btn" id="newTabBtn">+</button>
20
+ <div class="new-tab-menu" id="newTabMenu">
21
+ <div class="menu-item" data-type="agent">AGENT</div>
22
+ <div class="menu-item" data-type="code">CODE</div>
23
+ <div class="menu-item" data-type="research">RESEARCH</div>
24
+ <div class="menu-item" data-type="chat">CHAT</div>
25
+ </div>
26
+ </div>
27
+ <div class="tab-bar-spacer"></div>
28
+ <button class="settings-btn" id="settingsBtn">SETTINGS</button>
29
+ </div>
30
+
31
+ <!-- Tab Content Area -->
32
+ <div class="content-area">
33
+ <!-- Command Center Tab -->
34
+ <div class="tab-content active" data-content-id="0">
35
+ <div class="notebook-interface">
36
+ <div class="notebook-header">
37
+ <div>
38
+ <div class="notebook-type">COMMAND CENTER</div>
39
+ <h2>PRODUCTIVE</h2>
40
+ </div>
41
+ <div class="header-actions">
42
+ <button class="launcher-btn" data-type="agent">AGENT</button>
43
+ <button class="launcher-btn" data-type="code">CODE</button>
44
+ <button class="launcher-btn" data-type="research">RESEARCH</button>
45
+ <button class="launcher-btn" data-type="chat">CHAT</button>
46
+ </div>
47
+ </div>
48
+
49
+ <div class="notebook-body">
50
+ <div class="chat-container" id="messages-command">
51
+ <div class="welcome-message">
52
+ <p>Welcome to Productive — an AI interface with specialized notebooks.</p>
53
+ <p>The assistant can automatically open specialized notebooks for different tasks:</p>
54
+ <ul>
55
+ <li><strong>AGENT</strong> — Multi-step autonomous tasks</li>
56
+ <li><strong>CODE</strong> — Programming and data analysis</li>
57
+ <li><strong>RESEARCH</strong> — Deep research and information gathering</li>
58
+ <li><strong>CHAT</strong> — General conversation</li>
59
+ </ul>
60
+ <p>When a notebook is opened, you'll see a green widget you can click to jump to it. A pulsing dot on the tab indicates active generation.</p>
61
+ </div>
62
+ </div>
63
+ </div>
64
+
65
+ <div class="input-area">
66
+ <div class="input-container">
67
+ <input type="text" placeholder="Enter message..." id="input-command">
68
+ <button id="sendCommand">SEND</button>
69
+ </div>
70
+ </div>
71
+ </div>
72
+ </div>
73
+
74
+ <!-- Settings Tab -->
75
+ <div class="tab-content" data-content-id="settings">
76
+ <div class="settings-interface">
77
+ <div class="settings-header">
78
+ <h2>SETTINGS</h2>
79
+ </div>
80
+ <div class="settings-body">
81
+ <div class="settings-section">
82
+ <label class="settings-label">
83
+ <span class="label-text">LLM ENDPOINT</span>
84
+ <span class="label-description">OpenAI-compatible API endpoint (e.g., https://api.openai.com/v1)</span>
85
+ </label>
86
+ <input type="text" id="setting-endpoint" class="settings-input" placeholder="https://api.openai.com/v1">
87
+ </div>
88
+
89
+ <div class="settings-section">
90
+ <label class="settings-label">
91
+ <span class="label-text">API TOKEN (OPTIONAL)</span>
92
+ <span class="label-description">Authentication token for the LLM API</span>
93
+ </label>
94
+ <input type="password" id="setting-token" class="settings-input" placeholder="Leave empty if not required">
95
+ </div>
96
+
97
+ <div class="settings-section">
98
+ <label class="settings-label">
99
+ <span class="label-text">MODEL</span>
100
+ <span class="label-description">Model name (e.g., gpt-4, gpt-3.5-turbo)</span>
101
+ </label>
102
+ <input type="text" id="setting-model" class="settings-input" placeholder="gpt-4">
103
+ </div>
104
+
105
+ <div class="settings-actions">
106
+ <button class="settings-save-btn" id="saveSettingsBtn">SAVE</button>
107
+ <button class="settings-cancel-btn" id="cancelSettingsBtn">CANCEL</button>
108
+ </div>
109
+
110
+ <div class="settings-status" id="settingsStatus"></div>
111
+ </div>
112
+ </div>
113
+ </div>
114
+ </div>
115
+ </div>
116
+
117
+ <script src="script.js"></script>
118
+ </body>
119
+ </html>
script.js ADDED
@@ -0,0 +1,622 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // State management
2
+ let tabCounter = 1;
3
+ let activeTabId = 0;
4
+ let settings = {
5
+ endpoint: 'https://api.openai.com/v1',
6
+ token: '',
7
+ model: 'gpt-4'
8
+ };
9
+
10
+ // Track notebook counters for each type
11
+ let notebookCounters = {
12
+ 'agent': 0,
13
+ 'code': 0,
14
+ 'research': 0,
15
+ 'chat': 0
16
+ };
17
+
18
+ const notebookTitles = {
19
+ 'command-center': 'COMMAND',
20
+ 'agent': 'AGENT',
21
+ 'code': 'CODE',
22
+ 'research': 'RESEARCH',
23
+ 'chat': 'CHAT'
24
+ };
25
+
26
+ // Initialize
27
+ document.addEventListener('DOMContentLoaded', () => {
28
+ loadSettings();
29
+ initializeEventListeners();
30
+ });
31
+
32
+ function initializeEventListeners() {
33
+ // Launcher buttons in command center
34
+ document.querySelectorAll('.launcher-btn').forEach(btn => {
35
+ btn.addEventListener('click', (e) => {
36
+ e.stopPropagation();
37
+ const type = btn.dataset.type;
38
+ createNotebookTab(type);
39
+ });
40
+ });
41
+
42
+ // Command center chat functionality
43
+ const commandInput = document.getElementById('input-command');
44
+ const sendCommand = document.getElementById('sendCommand');
45
+ if (commandInput && sendCommand) {
46
+ sendCommand.addEventListener('click', () => sendMessage(0));
47
+ commandInput.addEventListener('keypress', (e) => {
48
+ if (e.key === 'Enter') sendMessage(0);
49
+ });
50
+ }
51
+
52
+ // New tab button - toggle menu
53
+ const newTabBtn = document.getElementById('newTabBtn');
54
+ const newTabMenu = document.getElementById('newTabMenu');
55
+
56
+ if (newTabBtn && newTabMenu) {
57
+ newTabBtn.addEventListener('click', (e) => {
58
+ e.stopPropagation();
59
+ e.preventDefault();
60
+
61
+ // Position the menu below the button
62
+ const rect = newTabBtn.getBoundingClientRect();
63
+ newTabMenu.style.top = `${rect.bottom}px`;
64
+ newTabMenu.style.left = `${rect.left}px`;
65
+
66
+ newTabMenu.classList.toggle('active');
67
+ });
68
+ }
69
+
70
+ // Close menu when clicking outside
71
+ document.addEventListener('click', (e) => {
72
+ if (!e.target.closest('.new-tab-wrapper')) {
73
+ newTabMenu.classList.remove('active');
74
+ }
75
+ });
76
+
77
+ // Menu items
78
+ document.querySelectorAll('.menu-item').forEach(item => {
79
+ item.addEventListener('click', () => {
80
+ const type = item.dataset.type;
81
+ createNotebookTab(type);
82
+ newTabMenu.classList.remove('active');
83
+ });
84
+ });
85
+
86
+ // Tab switching
87
+ document.addEventListener('click', (e) => {
88
+ const tab = e.target.closest('.tab');
89
+ if (tab && !e.target.closest('.new-tab-wrapper')) {
90
+ if (e.target.classList.contains('tab-close')) {
91
+ closeTab(parseInt(tab.dataset.tabId));
92
+ } else {
93
+ switchToTab(parseInt(tab.dataset.tabId));
94
+ }
95
+ }
96
+ });
97
+
98
+ // Settings button
99
+ const settingsBtn = document.getElementById('settingsBtn');
100
+ if (settingsBtn) {
101
+ settingsBtn.addEventListener('click', () => {
102
+ openSettings();
103
+ });
104
+ }
105
+
106
+ // Save settings button
107
+ const saveSettingsBtn = document.getElementById('saveSettingsBtn');
108
+ if (saveSettingsBtn) {
109
+ saveSettingsBtn.addEventListener('click', () => {
110
+ saveSettings();
111
+ });
112
+ }
113
+
114
+ // Cancel settings button
115
+ const cancelSettingsBtn = document.getElementById('cancelSettingsBtn');
116
+ if (cancelSettingsBtn) {
117
+ cancelSettingsBtn.addEventListener('click', () => {
118
+ switchToTab(0); // Go back to command center
119
+ });
120
+ }
121
+ }
122
+
123
+ function createNotebookTab(type, initialMessage = null, autoSwitch = true) {
124
+ const tabId = tabCounter++;
125
+
126
+ // Increment counter for this notebook type and create enumerated title
127
+ let title;
128
+ if (type !== 'command-center') {
129
+ notebookCounters[type]++;
130
+ title = `${notebookTitles[type]}-${notebookCounters[type]}`;
131
+ } else {
132
+ title = notebookTitles[type];
133
+ }
134
+
135
+ // Create tab
136
+ const tab = document.createElement('div');
137
+ tab.className = 'tab';
138
+ tab.dataset.tabId = tabId;
139
+ tab.innerHTML = `
140
+ <span class="tab-title">${title}</span>
141
+ <span class="tab-status" style="display: none;"></span>
142
+ <span class="tab-close">×</span>
143
+ `;
144
+
145
+ // Insert into the dynamic tabs container
146
+ const dynamicTabs = document.getElementById('dynamicTabs');
147
+ dynamicTabs.appendChild(tab);
148
+
149
+ // Create content
150
+ const content = document.createElement('div');
151
+ content.className = 'tab-content';
152
+ content.dataset.contentId = tabId;
153
+ content.innerHTML = createNotebookContent(type, tabId);
154
+
155
+ document.querySelector('.content-area').appendChild(content);
156
+
157
+ // Only switch to new tab if autoSwitch is true
158
+ if (autoSwitch) {
159
+ switchToTab(tabId);
160
+ }
161
+
162
+ // Add event listeners for the new content
163
+ if (type !== 'command-center') {
164
+ const input = content.querySelector('input');
165
+ const sendBtn = content.querySelector('.input-container button');
166
+
167
+ if (input && sendBtn) {
168
+ sendBtn.addEventListener('click', () => sendMessage(tabId));
169
+ input.addEventListener('keypress', (e) => {
170
+ if (e.key === 'Enter') sendMessage(tabId);
171
+ });
172
+ }
173
+
174
+ // If there's an initial message, automatically send it
175
+ if (initialMessage && input) {
176
+ input.value = initialMessage;
177
+ // Small delay to ensure everything is set up
178
+ setTimeout(() => {
179
+ sendMessage(tabId);
180
+ }, 100);
181
+ }
182
+ }
183
+
184
+ return tabId; // Return the tabId so we can reference it
185
+ }
186
+
187
+ function createNotebookContent(type, tabId) {
188
+ if (type === 'command-center') {
189
+ return document.querySelector('[data-content-id="0"]').innerHTML;
190
+ }
191
+
192
+ const placeholders = {
193
+ 'agent': 'Enter task for agent...',
194
+ 'code': 'Enter code query...',
195
+ 'research': 'Enter research topic...',
196
+ 'chat': 'Enter message...'
197
+ };
198
+
199
+ const fullTitles = {
200
+ 'agent': 'AGENT',
201
+ 'code': 'CODE',
202
+ 'research': 'RESEARCH',
203
+ 'chat': 'CHAT'
204
+ };
205
+
206
+ // Use unique ID combining type and tabId to ensure unique container IDs
207
+ const uniqueId = `${type}-${tabId}`;
208
+
209
+ return `
210
+ <div class="notebook-interface">
211
+ <div class="notebook-header">
212
+ <div>
213
+ <div class="notebook-type">${type.replace('-', ' ').toUpperCase()}</div>
214
+ <h2>${fullTitles[type]}</h2>
215
+ </div>
216
+ </div>
217
+ <div class="notebook-body">
218
+ <div class="chat-container" id="messages-${uniqueId}" data-notebook-type="${type}">
219
+ </div>
220
+ </div>
221
+ <div class="input-area">
222
+ <div class="input-container">
223
+ <input type="text" placeholder="${placeholders[type]}" id="input-${uniqueId}">
224
+ <button>SEND</button>
225
+ </div>
226
+ </div>
227
+ </div>
228
+ `;
229
+ }
230
+
231
+ function switchToTab(tabId) {
232
+ // Deactivate all tabs
233
+ document.querySelectorAll('.tab').forEach(tab => {
234
+ tab.classList.remove('active');
235
+ });
236
+ document.querySelectorAll('.tab-content').forEach(content => {
237
+ content.classList.remove('active');
238
+ });
239
+
240
+ // Activate selected tab
241
+ const tab = document.querySelector(`[data-tab-id="${tabId}"]`);
242
+ const content = document.querySelector(`[data-content-id="${tabId}"]`);
243
+
244
+ if (content) {
245
+ // For settings, there's no tab, just show the content
246
+ if (tabId === 'settings') {
247
+ content.classList.add('active');
248
+ activeTabId = tabId;
249
+ } else if (tab) {
250
+ tab.classList.add('active');
251
+ content.classList.add('active');
252
+ activeTabId = tabId;
253
+ }
254
+ }
255
+ }
256
+
257
+ function closeTab(tabId) {
258
+ if (tabId === 0) return; // Can't close command center
259
+
260
+ const tab = document.querySelector(`[data-tab-id="${tabId}"]`);
261
+ const content = document.querySelector(`[data-content-id="${tabId}"]`);
262
+
263
+ if (tab && content) {
264
+ tab.remove();
265
+ content.remove();
266
+
267
+ // Switch to command center if closing active tab
268
+ if (activeTabId === tabId) {
269
+ switchToTab(0);
270
+ }
271
+ }
272
+ }
273
+
274
+ async function sendMessage(tabId) {
275
+ const content = document.querySelector(`[data-content-id="${tabId}"]`);
276
+ if (!content) return;
277
+
278
+ const input = content.querySelector('input[type="text"]');
279
+ const chatContainer = content.querySelector('.chat-container');
280
+
281
+ if (!input || !chatContainer) return;
282
+
283
+ const message = input.value.trim();
284
+ if (!message) return;
285
+
286
+ // Remove welcome message if it exists (only on first user message)
287
+ const welcomeMsg = chatContainer.querySelector('.welcome-message');
288
+ if (welcomeMsg) {
289
+ welcomeMsg.remove();
290
+ }
291
+
292
+ // Add user message
293
+ const userMsg = document.createElement('div');
294
+ userMsg.className = 'message user';
295
+ userMsg.innerHTML = `
296
+ <div class="message-label">USER</div>
297
+ <div class="message-content">${escapeHtml(message)}</div>
298
+ `;
299
+ chatContainer.appendChild(userMsg);
300
+
301
+ // Clear input and disable it during processing
302
+ input.value = '';
303
+ input.disabled = true;
304
+
305
+ // Set tab to generating state
306
+ setTabGenerating(tabId, true);
307
+
308
+ // Create unique ID for this message
309
+ const messageId = `msg-${tabId}-${Date.now()}`;
310
+
311
+ // Create assistant message container
312
+ const assistantMsg = document.createElement('div');
313
+ assistantMsg.className = 'message assistant';
314
+ assistantMsg.innerHTML = `
315
+ <div class="message-label">ASSISTANT</div>
316
+ <div class="message-content" id="${messageId}"></div>
317
+ `;
318
+ chatContainer.appendChild(assistantMsg);
319
+ chatContainer.scrollTop = chatContainer.scrollHeight;
320
+
321
+ // Determine notebook type from chat container ID
322
+ const notebookType = getNotebookTypeFromContainer(chatContainer);
323
+
324
+ // Get conversation history (including the just-added user message)
325
+ const messages = getConversationHistory(chatContainer);
326
+
327
+ // Stream response from backend
328
+ await streamChatResponse(messageId, messages, chatContainer, notebookType, tabId);
329
+
330
+ // Re-enable input and mark generation as complete
331
+ input.disabled = false;
332
+ input.focus();
333
+ setTabGenerating(tabId, false);
334
+ }
335
+
336
+ function getNotebookTypeFromContainer(chatContainer) {
337
+ // Try to get type from data attribute first (for dynamically created notebooks)
338
+ const typeFromData = chatContainer.dataset.notebookType;
339
+ if (typeFromData) {
340
+ return typeFromData;
341
+ }
342
+
343
+ // Fallback: Extract notebook type from the container ID (e.g., "messages-command" -> "command")
344
+ const containerId = chatContainer.id;
345
+ if (containerId && containerId.startsWith('messages-')) {
346
+ const type = containerId.replace('messages-', '');
347
+ // Map to notebook type
348
+ if (type === 'command') return 'command';
349
+ if (type.startsWith('agent')) return 'agent';
350
+ if (type.startsWith('code')) return 'code';
351
+ if (type.startsWith('research')) return 'research';
352
+ if (type.startsWith('chat')) return 'chat';
353
+ }
354
+ return 'command'; // Default fallback
355
+ }
356
+
357
+ function getConversationHistory(chatContainer) {
358
+ const messages = [];
359
+ const messageElements = chatContainer.querySelectorAll('.message');
360
+
361
+ messageElements.forEach(msg => {
362
+ const label = msg.querySelector('.message-label')?.textContent.toLowerCase();
363
+ const content = msg.querySelector('.message-content')?.textContent;
364
+
365
+ if (label && content) {
366
+ if (label === 'user') {
367
+ messages.push({ role: 'user', content: content });
368
+ } else if (label === 'assistant') {
369
+ // Don't include the currently streaming message
370
+ if (!content.includes('streaming-content-')) {
371
+ messages.push({ role: 'assistant', content: content });
372
+ }
373
+ }
374
+ }
375
+ });
376
+
377
+ return messages;
378
+ }
379
+
380
+ async function streamChatResponse(messageId, messages, chatContainer, notebookType, tabId) {
381
+ const streamingContent = document.getElementById(messageId);
382
+ if (!streamingContent) return;
383
+
384
+ const currentSettings = getSettings();
385
+ const backendEndpoint = 'http://localhost:8000/api'; // Our FastAPI backend
386
+ const llmEndpoint = currentSettings.endpoint || 'https://api.openai.com/v1'; // User's LLM endpoint
387
+
388
+ try {
389
+ const response = await fetch(`${backendEndpoint}/chat/stream`, {
390
+ method: 'POST',
391
+ headers: {
392
+ 'Content-Type': 'application/json'
393
+ },
394
+ body: JSON.stringify({
395
+ messages: messages,
396
+ notebook_type: notebookType,
397
+ stream: true,
398
+ endpoint: llmEndpoint,
399
+ token: currentSettings.token || null,
400
+ model: currentSettings.model || 'gpt-4'
401
+ })
402
+ });
403
+
404
+ if (!response.ok) {
405
+ throw new Error(`HTTP error! status: ${response.status}`);
406
+ }
407
+
408
+ const reader = response.body.getReader();
409
+ const decoder = new TextDecoder();
410
+ let buffer = '';
411
+ let fullResponse = '';
412
+
413
+ while (true) {
414
+ const { done, value } = await reader.read();
415
+ if (done) break;
416
+
417
+ buffer += decoder.decode(value, { stream: true });
418
+ const lines = buffer.split('\n');
419
+ buffer = lines.pop() || '';
420
+
421
+ for (const line of lines) {
422
+ if (line.startsWith('data: ')) {
423
+ const data = JSON.parse(line.slice(6));
424
+
425
+ if (data.type === 'content') {
426
+ fullResponse += data.content;
427
+ streamingContent.innerHTML = parseMarkdown(fullResponse);
428
+ chatContainer.scrollTop = chatContainer.scrollHeight;
429
+ } else if (data.type === 'done') {
430
+ // Check for new action token format: <action:type>message</action>
431
+ const actionMatch = fullResponse.match(/<action:(agent|code|research|chat)>([\s\S]*?)<\/action>/i);
432
+ if (actionMatch) {
433
+ const action = actionMatch[1].toLowerCase();
434
+ const actionMessage = actionMatch[2].trim();
435
+
436
+ // Remove action token from display
437
+ fullResponse = fullResponse.replace(/<action:(agent|code|research|chat)>[\s\S]*?<\/action>/i, '').trim();
438
+
439
+ // If the response is empty after removing action token, remove the entire assistant message
440
+ if (fullResponse.length === 0) {
441
+ const assistantMsg = streamingContent.closest('.message.assistant');
442
+ if (assistantMsg) {
443
+ assistantMsg.remove();
444
+ }
445
+ } else {
446
+ streamingContent.innerHTML = parseMarkdown(fullResponse);
447
+ }
448
+
449
+ // Handle action and get the tab ID, then show widget with that ID
450
+ handleActionToken(action, actionMessage, (targetTabId) => {
451
+ showActionWidget(chatContainer, action, actionMessage, targetTabId);
452
+ });
453
+ }
454
+ } else if (data.type === 'error') {
455
+ streamingContent.innerHTML = `<span style="color: #c62828;">Error: ${escapeHtml(data.content)}</span>`;
456
+ }
457
+ }
458
+ }
459
+ }
460
+ } catch (error) {
461
+ streamingContent.textContent = `Connection error: ${error.message}. Check that the backend is running and the endpoint is correct in settings.`;
462
+ streamingContent.style.color = '#c62828';
463
+ // Clear generating state on error
464
+ if (tabId) {
465
+ setTabGenerating(tabId, false);
466
+ }
467
+ }
468
+ }
469
+
470
+ function showActionWidget(chatContainer, action, message, targetTabId) {
471
+ const widget = document.createElement('div');
472
+ widget.className = 'action-widget';
473
+ widget.style.cursor = 'pointer';
474
+ widget.dataset.targetTabId = targetTabId;
475
+ widget.innerHTML = `
476
+ <div class="action-widget-header">
477
+ <span class="action-widget-icon">→</span>
478
+ <span class="action-widget-text">Opening ${action.toUpperCase()} notebook...</span>
479
+ <span class="action-widget-type">${action}</span>
480
+ </div>
481
+ <div class="action-widget-message">"${escapeHtml(message)}"</div>
482
+ `;
483
+
484
+ // Make widget clickable to jump to the notebook
485
+ widget.addEventListener('click', () => {
486
+ switchToTab(parseInt(targetTabId));
487
+ });
488
+
489
+ chatContainer.appendChild(widget);
490
+ chatContainer.scrollTop = chatContainer.scrollHeight;
491
+ }
492
+
493
+ function handleActionToken(action, message, callback) {
494
+ // Open the notebook with the extracted message as initial prompt
495
+ // Don't auto-switch to the new tab (autoSwitch = false)
496
+ setTimeout(() => {
497
+ const tabId = createNotebookTab(action, message, false);
498
+ if (callback) {
499
+ callback(tabId);
500
+ }
501
+ }, 500);
502
+ }
503
+
504
+ function setTabGenerating(tabId, isGenerating) {
505
+ const tab = document.querySelector(`[data-tab-id="${tabId}"]`);
506
+ if (!tab) return;
507
+
508
+ const statusIndicator = tab.querySelector('.tab-status');
509
+ if (!statusIndicator) return;
510
+
511
+ if (isGenerating) {
512
+ statusIndicator.style.display = 'block';
513
+ statusIndicator.classList.add('generating');
514
+ } else {
515
+ statusIndicator.classList.remove('generating');
516
+ // Keep visible but stop animation
517
+ setTimeout(() => {
518
+ statusIndicator.style.display = 'none';
519
+ }, 300);
520
+ }
521
+ }
522
+
523
+ function escapeHtml(text) {
524
+ const div = document.createElement('div');
525
+ div.textContent = text;
526
+ return div.innerHTML;
527
+ }
528
+
529
+ function parseMarkdown(text) {
530
+ // Simple markdown parser for code blocks and inline code
531
+ let html = escapeHtml(text);
532
+
533
+ // Code blocks (```language\ncode\n```)
534
+ html = html.replace(/```(\w+)?\n([\s\S]*?)```/g, (match, lang, code) => {
535
+ return `<pre><code>${code.trim()}</code></pre>`;
536
+ });
537
+
538
+ // Inline code (`code`)
539
+ html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
540
+
541
+ // Bold (**text**)
542
+ html = html.replace(/\*\*([^\*]+)\*\*/g, '<strong>$1</strong>');
543
+
544
+ // Italic (*text*)
545
+ html = html.replace(/\*([^\*]+)\*/g, '<em>$1</em>');
546
+
547
+ return html;
548
+ }
549
+
550
+ // Settings management
551
+ function loadSettings() {
552
+ // Try to load from localStorage first
553
+ const savedSettings = localStorage.getItem('productive_settings');
554
+ if (savedSettings) {
555
+ try {
556
+ settings = JSON.parse(savedSettings);
557
+ } catch (e) {
558
+ console.error('Failed to parse settings:', e);
559
+ }
560
+ }
561
+ }
562
+
563
+ function openSettings() {
564
+ // Populate the settings form
565
+ document.getElementById('setting-endpoint').value = settings.endpoint || '';
566
+ document.getElementById('setting-token').value = settings.token || '';
567
+ document.getElementById('setting-model').value = settings.model || 'gpt-4';
568
+
569
+ // Clear any status message
570
+ const status = document.getElementById('settingsStatus');
571
+ status.className = 'settings-status';
572
+ status.textContent = '';
573
+
574
+ // Switch to settings view
575
+ switchToTab('settings');
576
+ }
577
+
578
+ function saveSettings() {
579
+ // Get values from form
580
+ const endpoint = document.getElementById('setting-endpoint').value.trim();
581
+ const token = document.getElementById('setting-token').value.trim();
582
+ const model = document.getElementById('setting-model').value.trim();
583
+
584
+ // Validate endpoint
585
+ if (!endpoint) {
586
+ showSettingsStatus('LLM Endpoint is required', 'error');
587
+ return;
588
+ }
589
+
590
+ // Validate model
591
+ if (!model) {
592
+ showSettingsStatus('Model name is required', 'error');
593
+ return;
594
+ }
595
+
596
+ // Update settings
597
+ settings.endpoint = endpoint;
598
+ settings.token = token;
599
+ settings.model = model;
600
+
601
+ // Save to localStorage
602
+ localStorage.setItem('productive_settings', JSON.stringify(settings));
603
+
604
+ // Show success message
605
+ showSettingsStatus('Settings saved successfully', 'success');
606
+
607
+ // Go back to command center after a short delay
608
+ setTimeout(() => {
609
+ switchToTab(0);
610
+ }, 1000);
611
+ }
612
+
613
+ function showSettingsStatus(message, type) {
614
+ const status = document.getElementById('settingsStatus');
615
+ status.textContent = message;
616
+ status.className = `settings-status ${type}`;
617
+ }
618
+
619
+ // Export settings for use in API calls
620
+ function getSettings() {
621
+ return settings;
622
+ }
style.css ADDED
@@ -0,0 +1,654 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ * {
2
+ margin: 0;
3
+ padding: 0;
4
+ box-sizing: border-box;
5
+ }
6
+
7
+ body {
8
+ font-family: 'JetBrains Mono', monospace;
9
+ background-color: #ffffff;
10
+ color: #1a1a1a;
11
+ height: 100vh;
12
+ overflow: hidden;
13
+ }
14
+
15
+ .app-container {
16
+ display: flex;
17
+ flex-direction: column;
18
+ height: 100vh;
19
+ }
20
+
21
+ /* Tab Bar */
22
+ .tab-bar {
23
+ display: flex;
24
+ align-items: stretch;
25
+ background: #f5f5f5;
26
+ padding: 0;
27
+ gap: 0;
28
+ overflow-x: auto;
29
+ overflow-y: visible;
30
+ border-bottom: 1px solid #ccc;
31
+ min-height: fit-content;
32
+ position: relative;
33
+ z-index: 10;
34
+ }
35
+
36
+ .tab-bar-spacer {
37
+ flex: 1;
38
+ }
39
+
40
+ #dynamicTabs {
41
+ display: flex;
42
+ align-items: stretch;
43
+ }
44
+
45
+ .tab {
46
+ display: flex;
47
+ align-items: center;
48
+ gap: 8px;
49
+ padding: 12px 20px;
50
+ background: #f5f5f5;
51
+ color: #666;
52
+ cursor: pointer;
53
+ border: none;
54
+ white-space: nowrap;
55
+ transition: all 0.2s;
56
+ border-right: 1px solid #ccc;
57
+ font-size: 12px;
58
+ font-weight: 500;
59
+ letter-spacing: 1px;
60
+ position: relative;
61
+ }
62
+
63
+ .tab:hover {
64
+ background: #eee;
65
+ color: #1a1a1a;
66
+ }
67
+
68
+ .tab.active {
69
+ background: #ffffff;
70
+ color: #1a1a1a;
71
+ border-bottom: 2px solid #1b5e20;
72
+ }
73
+
74
+ .tab-title {
75
+ font-size: 12px;
76
+ font-weight: 500;
77
+ }
78
+
79
+ .tab-close {
80
+ margin-left: 12px;
81
+ font-size: 14px;
82
+ opacity: 0.5;
83
+ transition: opacity 0.2s;
84
+ }
85
+
86
+ .tab-close:hover {
87
+ opacity: 1;
88
+ }
89
+
90
+ .tab-status {
91
+ width: 8px;
92
+ height: 8px;
93
+ border-radius: 50%;
94
+ background: #1b5e20;
95
+ margin-left: 4px;
96
+ }
97
+
98
+ .tab-status.generating {
99
+ background: #1b5e20;
100
+ animation: pulse 1.5s ease-in-out infinite;
101
+ }
102
+
103
+ @keyframes pulse {
104
+ 0%, 100% {
105
+ opacity: 1;
106
+ transform: scale(1);
107
+ }
108
+ 50% {
109
+ opacity: 0.5;
110
+ transform: scale(0.8);
111
+ }
112
+ }
113
+
114
+ .settings-btn {
115
+ background: #f5f5f5;
116
+ color: #666;
117
+ border: none;
118
+ border-right: 1px solid #ccc;
119
+ padding: 12px 20px;
120
+ font-size: 12px;
121
+ font-weight: 500;
122
+ letter-spacing: 1px;
123
+ cursor: pointer;
124
+ transition: all 0.2s;
125
+ font-family: inherit;
126
+ }
127
+
128
+ .settings-btn:hover {
129
+ background: #eee;
130
+ color: #1a1a1a;
131
+ }
132
+
133
+ .new-tab-wrapper {
134
+ position: static;
135
+ display: flex;
136
+ align-items: center;
137
+ }
138
+
139
+ .new-tab-btn {
140
+ background: #f5f5f5;
141
+ color: #666;
142
+ border: none;
143
+ border-right: 1px solid #ccc;
144
+ padding: 12px 20px;
145
+ font-size: 16px;
146
+ cursor: pointer;
147
+ transition: all 0.2s;
148
+ font-family: inherit;
149
+ }
150
+
151
+ .new-tab-btn:hover {
152
+ background: #eee;
153
+ color: #1a1a1a;
154
+ }
155
+
156
+ .new-tab-menu {
157
+ display: none;
158
+ position: fixed;
159
+ background: white;
160
+ border: 1px solid #ccc;
161
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
162
+ z-index: 9999;
163
+ min-width: 180px;
164
+ }
165
+
166
+ .new-tab-menu.active {
167
+ display: block;
168
+ }
169
+
170
+ .menu-item {
171
+ padding: 12px 16px;
172
+ font-size: 12px;
173
+ font-weight: 500;
174
+ letter-spacing: 0.5px;
175
+ cursor: pointer;
176
+ border-bottom: 1px solid #eee;
177
+ transition: all 0.2s;
178
+ color: #1a1a1a;
179
+ background: white;
180
+ }
181
+
182
+ .menu-item:last-child {
183
+ border-bottom: none;
184
+ }
185
+
186
+ .menu-item:hover {
187
+ background: #1b5e20;
188
+ color: white;
189
+ }
190
+
191
+ /* Content Area */
192
+ .content-area {
193
+ flex: 1;
194
+ overflow: hidden;
195
+ position: relative;
196
+ z-index: 1;
197
+ }
198
+
199
+ .tab-content {
200
+ display: none;
201
+ height: 100%;
202
+ overflow-y: auto;
203
+ padding: 20px;
204
+ }
205
+
206
+ .tab-content.active {
207
+ display: flex;
208
+ flex-direction: column;
209
+ }
210
+
211
+ /* Launcher Buttons */
212
+ .launcher-btn {
213
+ background: white;
214
+ border: 1px solid #ccc;
215
+ color: #1a1a1a;
216
+ padding: 6px 12px;
217
+ font-family: inherit;
218
+ font-size: 10px;
219
+ font-weight: 500;
220
+ border-radius: 4px;
221
+ cursor: pointer;
222
+ transition: all 0.2s;
223
+ letter-spacing: 0.5px;
224
+ white-space: nowrap;
225
+ }
226
+
227
+ .launcher-btn:hover {
228
+ background: #1b5e20;
229
+ color: #ffffff;
230
+ border-color: #1b5e20;
231
+ }
232
+
233
+ .launcher-btn:active {
234
+ transform: translateY(1px);
235
+ }
236
+
237
+ /* Notebook Content */
238
+ .notebook-interface {
239
+ height: 100%;
240
+ display: flex;
241
+ flex-direction: column;
242
+ }
243
+
244
+ .notebook-header {
245
+ background: #f5f5f5;
246
+ padding: 15px 20px;
247
+ border-bottom: 1px solid #ccc;
248
+ display: flex;
249
+ justify-content: space-between;
250
+ align-items: center;
251
+ gap: 20px;
252
+ }
253
+
254
+ .header-actions {
255
+ display: flex;
256
+ gap: 10px;
257
+ align-items: center;
258
+ }
259
+
260
+ .notebook-type {
261
+ font-size: 10px;
262
+ color: #666;
263
+ text-transform: uppercase;
264
+ letter-spacing: 1px;
265
+ }
266
+
267
+ .notebook-header h2 {
268
+ font-size: 14px;
269
+ font-weight: 500;
270
+ letter-spacing: 1px;
271
+ color: #1a1a1a;
272
+ margin-top: 4px;
273
+ }
274
+
275
+ .notebook-body {
276
+ flex: 1;
277
+ background: white;
278
+ padding: 20px;
279
+ overflow-y: auto;
280
+ }
281
+
282
+ .chat-container {
283
+ max-width: 900px;
284
+ margin: 0 auto;
285
+ font-size: 13px;
286
+ }
287
+
288
+ .welcome-message {
289
+ color: #999;
290
+ font-size: 13px;
291
+ line-height: 1.8;
292
+ max-width: 700px;
293
+ margin: 0 auto;
294
+ padding: 40px 20px;
295
+ }
296
+
297
+ .welcome-message p {
298
+ margin-bottom: 16px;
299
+ }
300
+
301
+ .welcome-message ul {
302
+ list-style: none;
303
+ padding-left: 0;
304
+ margin: 12px 0;
305
+ }
306
+
307
+ .welcome-message li {
308
+ margin: 8px 0;
309
+ padding-left: 20px;
310
+ position: relative;
311
+ }
312
+
313
+ .welcome-message li::before {
314
+ content: "•";
315
+ position: absolute;
316
+ left: 0;
317
+ color: #1b5e20;
318
+ }
319
+
320
+ .welcome-message strong {
321
+ color: #666;
322
+ font-weight: 500;
323
+ }
324
+
325
+ .message {
326
+ margin-bottom: 16px;
327
+ display: flex;
328
+ flex-direction: column;
329
+ gap: 4px;
330
+ }
331
+
332
+ .message-label {
333
+ font-size: 10px;
334
+ font-weight: 500;
335
+ color: #666;
336
+ text-transform: uppercase;
337
+ letter-spacing: 1px;
338
+ }
339
+
340
+ .message-content {
341
+ padding: 12px 15px;
342
+ background: #f5f5f5;
343
+ border: 1px solid #ccc;
344
+ border-radius: 4px;
345
+ line-height: 1.6;
346
+ white-space: pre-wrap;
347
+ word-wrap: break-word;
348
+ }
349
+
350
+ .message-content code {
351
+ background: #e0e0e0;
352
+ padding: 2px 6px;
353
+ border-radius: 3px;
354
+ font-family: 'JetBrains Mono', monospace;
355
+ font-size: 12px;
356
+ }
357
+
358
+ .message-content pre {
359
+ background: #2c2c2c;
360
+ color: #f8f8f8;
361
+ padding: 12px;
362
+ border-radius: 4px;
363
+ overflow-x: auto;
364
+ margin: 8px 0;
365
+ }
366
+
367
+ .message-content pre code {
368
+ background: transparent;
369
+ padding: 0;
370
+ color: #f8f8f8;
371
+ }
372
+
373
+ .message.user .message-content {
374
+ background: #eee;
375
+ }
376
+
377
+ .message.assistant .message-content {
378
+ background: #f5f5f5;
379
+ }
380
+
381
+ .input-area {
382
+ padding: 15px 20px;
383
+ background: white;
384
+ border-top: 1px solid #ccc;
385
+ }
386
+
387
+ .input-container {
388
+ max-width: 900px;
389
+ margin: 0 auto;
390
+ display: flex;
391
+ gap: 12px;
392
+ }
393
+
394
+ .input-container input {
395
+ flex: 1;
396
+ padding: 12px 15px;
397
+ border: 1px solid #ccc;
398
+ border-radius: 4px;
399
+ font-family: inherit;
400
+ font-size: 13px;
401
+ background: #f5f5f5;
402
+ color: #1a1a1a;
403
+ outline: none;
404
+ transition: border-color 0.2s ease;
405
+ }
406
+
407
+ .input-container input:focus {
408
+ border-color: #1b5e20;
409
+ box-shadow: 0 0 0 1px #1b5e20;
410
+ }
411
+
412
+ .input-container input::placeholder {
413
+ color: #999;
414
+ }
415
+
416
+ .input-container button {
417
+ padding: 12px 24px;
418
+ background: #f5f5f5;
419
+ border: 1px solid #ccc;
420
+ color: #1a1a1a;
421
+ font-family: inherit;
422
+ font-size: 12px;
423
+ font-weight: 500;
424
+ border-radius: 4px;
425
+ cursor: pointer;
426
+ transition: all 0.2s;
427
+ letter-spacing: 0.5px;
428
+ }
429
+
430
+ .input-container button:hover {
431
+ background: #1b5e20;
432
+ color: #ffffff;
433
+ border-color: #1b5e20;
434
+ }
435
+
436
+ .input-container button:active {
437
+ transform: translateY(1px);
438
+ }
439
+
440
+ /* Scrollbar styling */
441
+ .notebook-body::-webkit-scrollbar,
442
+ .tab-content::-webkit-scrollbar {
443
+ width: 6px;
444
+ }
445
+
446
+ .notebook-body::-webkit-scrollbar-track,
447
+ .tab-content::-webkit-scrollbar-track {
448
+ background: #f5f5f5;
449
+ }
450
+
451
+ .notebook-body::-webkit-scrollbar-thumb,
452
+ .tab-content::-webkit-scrollbar-thumb {
453
+ background: #ccc;
454
+ border-radius: 3px;
455
+ }
456
+
457
+ /* Settings Interface */
458
+ .settings-interface {
459
+ height: 100%;
460
+ display: flex;
461
+ flex-direction: column;
462
+ background: white;
463
+ }
464
+
465
+ .settings-header {
466
+ background: #f5f5f5;
467
+ padding: 20px;
468
+ border-bottom: 1px solid #ccc;
469
+ }
470
+
471
+ .settings-header h2 {
472
+ font-size: 18px;
473
+ font-weight: 500;
474
+ letter-spacing: 2px;
475
+ color: #1a1a1a;
476
+ }
477
+
478
+ .settings-body {
479
+ flex: 1;
480
+ padding: 40px;
481
+ overflow-y: auto;
482
+ max-width: 800px;
483
+ }
484
+
485
+ .settings-section {
486
+ margin-bottom: 32px;
487
+ }
488
+
489
+ .settings-label {
490
+ display: flex;
491
+ flex-direction: column;
492
+ gap: 4px;
493
+ margin-bottom: 8px;
494
+ }
495
+
496
+ .label-text {
497
+ font-size: 11px;
498
+ font-weight: 500;
499
+ color: #1a1a1a;
500
+ letter-spacing: 1px;
501
+ }
502
+
503
+ .label-description {
504
+ font-size: 11px;
505
+ color: #666;
506
+ font-weight: 300;
507
+ }
508
+
509
+ .settings-input {
510
+ width: 100%;
511
+ padding: 12px 15px;
512
+ border: 1px solid #ccc;
513
+ border-radius: 4px;
514
+ font-family: inherit;
515
+ font-size: 13px;
516
+ background: #f5f5f5;
517
+ color: #1a1a1a;
518
+ outline: none;
519
+ transition: border-color 0.2s ease;
520
+ }
521
+
522
+ .settings-input:focus {
523
+ border-color: #1b5e20;
524
+ box-shadow: 0 0 0 1px #1b5e20;
525
+ background: white;
526
+ }
527
+
528
+ .settings-input::placeholder {
529
+ color: #999;
530
+ }
531
+
532
+ .settings-actions {
533
+ display: flex;
534
+ gap: 12px;
535
+ margin-top: 32px;
536
+ }
537
+
538
+ .settings-save-btn,
539
+ .settings-cancel-btn {
540
+ padding: 12px 24px;
541
+ font-family: inherit;
542
+ font-size: 12px;
543
+ font-weight: 500;
544
+ border-radius: 4px;
545
+ cursor: pointer;
546
+ transition: all 0.2s;
547
+ letter-spacing: 0.5px;
548
+ border: 1px solid #ccc;
549
+ }
550
+
551
+ .settings-save-btn {
552
+ background: #1b5e20;
553
+ color: white;
554
+ border-color: #1b5e20;
555
+ }
556
+
557
+ .settings-save-btn:hover {
558
+ background: #145017;
559
+ }
560
+
561
+ .settings-cancel-btn {
562
+ background: #f5f5f5;
563
+ color: #1a1a1a;
564
+ }
565
+
566
+ .settings-cancel-btn:hover {
567
+ background: #eee;
568
+ }
569
+
570
+ .settings-status {
571
+ margin-top: 16px;
572
+ font-size: 12px;
573
+ padding: 12px;
574
+ border-radius: 4px;
575
+ display: none;
576
+ }
577
+
578
+ .settings-status.success {
579
+ display: block;
580
+ background: #e8f5e9;
581
+ color: #1b5e20;
582
+ border: 1px solid #1b5e20;
583
+ }
584
+
585
+ .settings-status.error {
586
+ display: block;
587
+ background: #ffebee;
588
+ color: #c62828;
589
+ border: 1px solid #c62828;
590
+ }
591
+
592
+ /* Action Widget */
593
+ .action-widget {
594
+ margin: 8px 0;
595
+ padding: 10px 12px;
596
+ background: #e8f5e9;
597
+ border: 1px solid #1b5e20;
598
+ border-left: 4px solid #1b5e20;
599
+ border-radius: 4px;
600
+ font-size: 12px;
601
+ display: flex;
602
+ flex-direction: column;
603
+ gap: 6px;
604
+ transition: all 0.2s;
605
+ }
606
+
607
+ .action-widget:hover {
608
+ background: #c8e6c9;
609
+ transform: translateX(2px);
610
+ }
611
+
612
+ .action-widget-header {
613
+ display: flex;
614
+ align-items: center;
615
+ gap: 8px;
616
+ }
617
+
618
+ .action-widget-icon {
619
+ font-weight: 700;
620
+ color: #1b5e20;
621
+ }
622
+
623
+ .action-widget-text {
624
+ flex: 1;
625
+ color: #1a1a1a;
626
+ }
627
+
628
+ .action-widget-type {
629
+ font-weight: 500;
630
+ text-transform: uppercase;
631
+ color: #1b5e20;
632
+ font-size: 10px;
633
+ letter-spacing: 0.5px;
634
+ }
635
+
636
+ .action-widget-message {
637
+ font-size: 10px;
638
+ color: #666;
639
+ padding: 6px 8px;
640
+ background: rgba(27, 94, 32, 0.05);
641
+ border-radius: 3px;
642
+ font-style: italic;
643
+ line-height: 1.4;
644
+ }
645
+
646
+ @media (max-width: 1024px) {
647
+ .notebook-types {
648
+ grid-template-columns: 1fr;
649
+ }
650
+
651
+ .settings-body {
652
+ padding: 20px;
653
+ }
654
+ }