RobertoBarrosoLuque commited on
Commit
8893dd6
·
1 Parent(s): c358e46

design ready

Browse files
Files changed (8) hide show
  1. Makefile +1 -2
  2. README.md +2 -2
  3. assets/kimi-logo.png +3 -0
  4. assets/qwen-logo.png +3 -0
  5. requirements.txt +4 -5
  6. src/__init__.py +0 -0
  7. src/app.py +874 -0
  8. src/config.py +287 -0
Makefile CHANGED
@@ -21,5 +21,4 @@ clean:
21
  find . -type d -name .ipynb_checkpoints -exec rm -rf {} +
22
 
23
  run:
24
- @. .venv/bin/activate
25
- python -m src.app
 
21
  find . -type d -name .ipynb_checkpoints -exec rm -rf {} +
22
 
23
  run:
24
+ .venv/bin/python src/app.py
 
README.md CHANGED
@@ -1,8 +1,8 @@
1
  ---
2
  title: Fw Vision Studio
3
  emoji: ⚡
4
- colorFrom: pink
5
- colorTo: blue
6
  sdk: gradio
7
  sdk_version: 6.6.0
8
  app_file: app.py
 
1
  ---
2
  title: Fw Vision Studio
3
  emoji: ⚡
4
+ colorFrom: blue
5
+ colorTo: purple
6
  sdk: gradio
7
  sdk_version: 6.6.0
8
  app_file: app.py
assets/kimi-logo.png ADDED

Git LFS Details

  • SHA256: be06357bae23718e1f35b358d96faf3c57b3c24edcde30cc61aef4a892b1ea3b
  • Pointer size: 129 Bytes
  • Size of remote file: 1.94 kB
assets/qwen-logo.png ADDED

Git LFS Details

  • SHA256: 683ed1e1227b7558e5bdd28b5b21d9c9912f096a4d0380ea854c4a7763087f41
  • Pointer size: 131 Bytes
  • Size of remote file: 195 kB
requirements.txt CHANGED
@@ -1,8 +1,7 @@
1
  huggingface_hub==0.34.3
2
  fireworks-ai==0.19.18
3
  python-dotenv==1.0.0
4
- ipython
5
- scikit-learn
6
- jupyter
7
- altair
8
- matplotlib
 
1
  huggingface_hub==0.34.3
2
  fireworks-ai==0.19.18
3
  python-dotenv==1.0.0
4
+ gradio==6.6.0
5
+ modelscope_studio==1.3.1
6
+ PyMuPDF
7
+ Pillow>=10.0.0
 
src/__init__.py ADDED
File without changes
src/app.py ADDED
@@ -0,0 +1,874 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Fireworks Vision Studio - Chat interface for Document Intelligence & Design to Code."""
2
+
3
+ import base64
4
+ import os
5
+ import tempfile
6
+ import time
7
+ import uuid
8
+
9
+ import fitz # PyMuPDF
10
+ import gradio as gr
11
+ from gradio_client import utils as client_utils
12
+ import modelscope_studio.components.antd as antd
13
+ import modelscope_studio.components.antdx as antdx
14
+ import modelscope_studio.components.base as ms
15
+ import modelscope_studio.components.pro as pro
16
+ from modelscope_studio.components.pro.chatbot import (
17
+ ChatbotActionConfig,
18
+ ChatbotBotConfig,
19
+ ChatbotMarkdownConfig,
20
+ ChatbotUserConfig,
21
+ ChatbotWelcomeConfig,
22
+ )
23
+ from modelscope_studio.components.pro.multimodal_input import (
24
+ MultimodalInputUploadConfig,
25
+ )
26
+ from openai import OpenAI
27
+ from PIL import Image
28
+
29
+ try:
30
+ from dotenv import load_dotenv
31
+
32
+ load_dotenv()
33
+ except ImportError:
34
+ pass
35
+
36
+ # ---------------------------------------------------------------------------
37
+ # Config
38
+ # ---------------------------------------------------------------------------
39
+
40
+ MODELS = {
41
+ "kimi-k2p5": {
42
+ "label": "Kimi K2.5",
43
+ "id": "accounts/fireworks/models/kimi-k2p5",
44
+ "icon": "./assets/kimi-logo.png",
45
+ },
46
+ "qwen3-vl-30b": {
47
+ "label": "Qwen3 VL 30B",
48
+ "id": "accounts/fireworks/models/qwen3-vl-30b-a3b-instruct",
49
+ "icon": "./assets/qwen-logo.png",
50
+ },
51
+ }
52
+ DEFAULT_MODEL_KEY = "kimi-k2p5"
53
+
54
+ MAX_IMAGE_DIM = 2048
55
+ MAX_PDF_PAGES = 10
56
+ PDF_DPI = 200
57
+
58
+ DEFAULT_SYS_PROMPT = (
59
+ "You are Fireworks Vision Studio, a highly capable multimodal AI assistant. "
60
+ "You excel at document analysis (legal, medical, financial) and converting "
61
+ "designs/wireframes into code. Be thorough, accurate, and well-structured. "
62
+ "Use markdown formatting for clarity. When generating HTML/CSS, return a "
63
+ "single self-contained file wrapped in a ```html code fence."
64
+ )
65
+
66
+ DEFAULT_THEME = {
67
+ "token": {
68
+ "colorPrimary": "#6d28d9",
69
+ }
70
+ }
71
+
72
+
73
+ def get_client() -> OpenAI:
74
+ return OpenAI(
75
+ base_url="https://api.fireworks.ai/inference/v1",
76
+ api_key=os.environ.get("FIREWORKS_API_KEY", ""),
77
+ )
78
+
79
+
80
+ # ---------------------------------------------------------------------------
81
+ # Image / PDF utilities
82
+ # ---------------------------------------------------------------------------
83
+
84
+
85
+ def encode_file_to_base64(file_path: str) -> str:
86
+ """Encode a file to a base64 data URI, resizing images if needed."""
87
+ mime_type = client_utils.get_mimetype(file_path)
88
+
89
+ # Resize large images
90
+ if mime_type and mime_type.startswith("image"):
91
+ img = Image.open(file_path)
92
+ if max(img.size) > MAX_IMAGE_DIM:
93
+ img.thumbnail((MAX_IMAGE_DIM, MAX_IMAGE_DIM), Image.LANCZOS)
94
+ tmp = tempfile.NamedTemporaryFile(suffix=".png", delete=False)
95
+ img.save(tmp.name, "PNG")
96
+ file_path = tmp.name
97
+ mime_type = "image/png"
98
+
99
+ with open(file_path, "rb") as f:
100
+ b64 = base64.b64encode(f.read()).decode("utf-8")
101
+ return f"data:{mime_type};base64,{b64}"
102
+
103
+
104
+ def pdf_to_images(pdf_path: str) -> list[str]:
105
+ """Convert PDF pages to temporary PNG files via PyMuPDF."""
106
+ doc = fitz.open(pdf_path)
107
+ paths = []
108
+ for i, page in enumerate(doc):
109
+ if i >= MAX_PDF_PAGES:
110
+ break
111
+ mat = fitz.Matrix(PDF_DPI / 72, PDF_DPI / 72)
112
+ pix = page.get_pixmap(matrix=mat)
113
+ tmp = tempfile.NamedTemporaryFile(suffix=".png", delete=False)
114
+ pix.save(tmp.name)
115
+ paths.append(tmp.name)
116
+ doc.close()
117
+ return paths
118
+
119
+
120
+ # ---------------------------------------------------------------------------
121
+ # Message formatting
122
+ # ---------------------------------------------------------------------------
123
+
124
+
125
+ def format_history(history):
126
+ """Convert chatbot history to OpenAI-compatible messages."""
127
+ messages = [{"role": "system", "content": DEFAULT_SYS_PROMPT}]
128
+
129
+ for item in history:
130
+ if item["role"] == "user":
131
+ files = []
132
+ for file_path in item["content"][0]["content"]:
133
+ if os.path.exists(file_path):
134
+ mime_type = client_utils.get_mimetype(file_path)
135
+
136
+ # Handle PDFs: convert to images
137
+ if mime_type == "application/pdf":
138
+ for img_path in pdf_to_images(file_path):
139
+ data_uri = encode_file_to_base64(img_path)
140
+ files.append(
141
+ {
142
+ "type": "image_url",
143
+ "image_url": {"url": data_uri},
144
+ }
145
+ )
146
+ elif mime_type and mime_type.startswith("image"):
147
+ data_uri = encode_file_to_base64(file_path)
148
+ files.append(
149
+ {
150
+ "type": "image_url",
151
+ "image_url": {"url": data_uri},
152
+ }
153
+ )
154
+ elif file_path.startswith("http"):
155
+ files.append(
156
+ {"type": "image_url", "image_url": {"url": file_path}}
157
+ )
158
+
159
+ text = item["content"][1]["content"]
160
+ messages.append(
161
+ {"role": "user", "content": files + [{"type": "text", "text": text}]}
162
+ )
163
+ elif item["role"] == "assistant":
164
+ contents = [
165
+ c for c in item["content"] if c.get("type") == "text"
166
+ ]
167
+ messages.append(
168
+ {
169
+ "role": "assistant",
170
+ "content": contents[0]["content"] if contents else "",
171
+ }
172
+ )
173
+
174
+ return messages
175
+
176
+
177
+ # ---------------------------------------------------------------------------
178
+ # UI config helpers
179
+ # ---------------------------------------------------------------------------
180
+
181
+
182
+ def user_config(disabled_actions=None):
183
+ return ChatbotUserConfig(
184
+ class_names=dict(content="user-message-content"),
185
+ actions=[
186
+ "copy",
187
+ "edit",
188
+ ChatbotActionConfig(
189
+ action="delete",
190
+ popconfirm=dict(
191
+ title="Delete message",
192
+ description="Are you sure you want to delete this message?",
193
+ okButtonProps=dict(danger=True),
194
+ ),
195
+ ),
196
+ ],
197
+ disabled_actions=disabled_actions,
198
+ )
199
+
200
+
201
+ def bot_config(disabled_actions=None, model_key=DEFAULT_MODEL_KEY):
202
+ model = MODELS[model_key]
203
+ return ChatbotBotConfig(
204
+ actions=[
205
+ "copy",
206
+ "edit",
207
+ ChatbotActionConfig(
208
+ action="retry",
209
+ popconfirm=dict(
210
+ title="Regenerate message",
211
+ description="This will also delete all subsequent messages.",
212
+ okButtonProps=dict(danger=True),
213
+ ),
214
+ ),
215
+ ChatbotActionConfig(
216
+ action="delete",
217
+ popconfirm=dict(
218
+ title="Delete message",
219
+ description="Are you sure you want to delete this message?",
220
+ okButtonProps=dict(danger=True),
221
+ ),
222
+ ),
223
+ ],
224
+ avatar=model["icon"],
225
+ disabled_actions=disabled_actions,
226
+ )
227
+
228
+
229
+ def welcome_config(model_key=DEFAULT_MODEL_KEY):
230
+ model = MODELS[model_key]
231
+ return ChatbotWelcomeConfig(
232
+ variant="borderless",
233
+ icon=model["icon"],
234
+ title=f"Hello, I'm {model['label']}",
235
+ description="Upload images or PDFs and start a conversation. I specialize in document analysis and converting designs to code.",
236
+ prompts=dict(
237
+ title="How can I help you today?",
238
+ styles={
239
+ "list": {"width": "100%"},
240
+ "item": {"flex": 1},
241
+ },
242
+ items=[
243
+ {
244
+ "label": "📄 Document Intelligence",
245
+ "children": [
246
+ {
247
+ "description": "Extract all parties, dates, and key obligations from this contract.",
248
+ },
249
+ {
250
+ "description": "Summarize this medical report. List diagnoses, medications, and follow-up actions.",
251
+ },
252
+ {
253
+ "description": "Extract all line items and totals from this invoice into a markdown table.",
254
+ },
255
+ ],
256
+ },
257
+ {
258
+ "label": "🎨 Design to Code",
259
+ "children": [
260
+ {
261
+ "description": "Convert this wireframe into a responsive HTML page with a modern look.",
262
+ },
263
+ {
264
+ "description": "Build this as a landing page with hero section, features grid, and footer using HTML and CSS.",
265
+ },
266
+ {
267
+ "description": "Create a responsive dashboard layout matching this sketch with HTML, CSS, and inline JS.",
268
+ },
269
+ ],
270
+ },
271
+ ],
272
+ ),
273
+ )
274
+
275
+
276
+ def markdown_config():
277
+ return ChatbotMarkdownConfig()
278
+
279
+
280
+ def upload_config():
281
+ return MultimodalInputUploadConfig(
282
+ accept="image/*,.pdf",
283
+ placeholder={
284
+ "inline": {
285
+ "title": "Upload files",
286
+ "description": "Click or drag files to upload images or PDFs",
287
+ },
288
+ "drop": {
289
+ "title": "Drop files here",
290
+ },
291
+ },
292
+ )
293
+
294
+
295
+ # ---------------------------------------------------------------------------
296
+ # Event handlers
297
+ # ---------------------------------------------------------------------------
298
+
299
+
300
+ class Events:
301
+ @staticmethod
302
+ def submit(state_value):
303
+ history = state_value["conversation_contexts"][
304
+ state_value["conversation_id"]
305
+ ]["history"]
306
+ model_key = state_value.get("model_key", DEFAULT_MODEL_KEY)
307
+ model_info = MODELS[model_key]
308
+ messages = format_history(history)
309
+
310
+ history.append(
311
+ {
312
+ "role": "assistant",
313
+ "content": [],
314
+ "key": str(uuid.uuid4()),
315
+ "loading": True,
316
+ "header": model_info["label"],
317
+ "status": "pending",
318
+ }
319
+ )
320
+ yield {
321
+ chatbot: gr.update(value=history),
322
+ state: gr.update(value=state_value),
323
+ }
324
+
325
+ try:
326
+ client = get_client()
327
+ response = client.chat.completions.create(
328
+ model=model_info["id"],
329
+ messages=messages,
330
+ stream=True,
331
+ max_tokens=4096,
332
+ )
333
+
334
+ start_time = time.time()
335
+ answer_content = ""
336
+ contents = [None]
337
+
338
+ for chunk in response:
339
+ if not chunk or not chunk.choices[0].delta.content:
340
+ continue
341
+
342
+ delta = chunk.choices[0].delta
343
+ if delta.content:
344
+ if contents[0] is None:
345
+ contents[0] = {"type": "text", "content": ""}
346
+ answer_content += delta.content
347
+ contents[0]["content"] = answer_content
348
+
349
+ history[-1]["content"] = [c for c in contents if c]
350
+ history[-1]["loading"] = False
351
+ yield {
352
+ chatbot: gr.update(value=history),
353
+ state: gr.update(value=state_value),
354
+ }
355
+
356
+ history[-1]["status"] = "done"
357
+ cost_time = f"{time.time() - start_time:.2f}"
358
+ history[-1]["footer"] = f"{cost_time}s"
359
+ yield {
360
+ chatbot: gr.update(value=history),
361
+ state: gr.update(value=state_value),
362
+ }
363
+ except Exception as e:
364
+ history[-1]["loading"] = False
365
+ history[-1]["status"] = "done"
366
+ history[-1]["content"] += [
367
+ {
368
+ "type": "text",
369
+ "content": f'<span style="color: var(--color-red-500)">{e}</span>',
370
+ }
371
+ ]
372
+ yield {
373
+ chatbot: gr.update(value=history),
374
+ state: gr.update(value=state_value),
375
+ }
376
+ raise
377
+
378
+ @staticmethod
379
+ def add_message(input_value, state_value):
380
+ text = input_value["text"]
381
+ files = input_value["files"]
382
+
383
+ if not state_value["conversation_id"]:
384
+ random_id = str(uuid.uuid4())
385
+ state_value["conversation_id"] = random_id
386
+ state_value["conversation_contexts"][random_id] = {"history": []}
387
+ state_value["conversations"].append(
388
+ {"label": text[:50], "key": random_id}
389
+ )
390
+
391
+ history = state_value["conversation_contexts"][
392
+ state_value["conversation_id"]
393
+ ]["history"]
394
+
395
+ history.append(
396
+ {
397
+ "key": str(uuid.uuid4()),
398
+ "role": "user",
399
+ "content": [
400
+ {"type": "file", "content": [f for f in files]},
401
+ {"type": "text", "content": text},
402
+ ],
403
+ }
404
+ )
405
+
406
+ yield Events.preprocess_submit(clear_input=True)(state_value)
407
+ try:
408
+ for chunk in Events.submit(state_value):
409
+ yield chunk
410
+ except Exception:
411
+ raise
412
+ finally:
413
+ yield Events.postprocess_submit(state_value)
414
+
415
+ @staticmethod
416
+ def preprocess_submit(clear_input=True):
417
+ def handler(state_value):
418
+ history = state_value["conversation_contexts"][
419
+ state_value["conversation_id"]
420
+ ]["history"]
421
+ return {
422
+ **(
423
+ {input: gr.update(value=None, loading=True)}
424
+ if clear_input
425
+ else {}
426
+ ),
427
+ conversations: gr.update(
428
+ active_key=state_value["conversation_id"],
429
+ items=list(
430
+ map(
431
+ lambda item: {
432
+ **item,
433
+ "disabled": item["key"]
434
+ != state_value["conversation_id"],
435
+ },
436
+ state_value["conversations"],
437
+ )
438
+ ),
439
+ ),
440
+ add_conversation_btn: gr.update(disabled=True),
441
+ clear_btn: gr.update(disabled=True),
442
+ conversation_delete_menu_item: gr.update(disabled=True),
443
+ chatbot: gr.update(
444
+ value=history,
445
+ bot_config=bot_config(
446
+ disabled_actions=["edit", "retry", "delete"],
447
+ model_key=state_value.get("model_key", DEFAULT_MODEL_KEY),
448
+ ),
449
+ user_config=user_config(
450
+ disabled_actions=["edit", "delete"]
451
+ ),
452
+ ),
453
+ state: gr.update(value=state_value),
454
+ }
455
+
456
+ return handler
457
+
458
+ @staticmethod
459
+ def postprocess_submit(state_value):
460
+ history = state_value["conversation_contexts"][
461
+ state_value["conversation_id"]
462
+ ]["history"]
463
+ mk = state_value.get("model_key", DEFAULT_MODEL_KEY)
464
+ return {
465
+ input: gr.update(loading=False),
466
+ conversation_delete_menu_item: gr.update(disabled=False),
467
+ clear_btn: gr.update(disabled=False),
468
+ conversations: gr.update(items=state_value["conversations"]),
469
+ add_conversation_btn: gr.update(disabled=False),
470
+ chatbot: gr.update(
471
+ value=history,
472
+ bot_config=bot_config(model_key=mk),
473
+ user_config=user_config(),
474
+ ),
475
+ state: gr.update(value=state_value),
476
+ }
477
+
478
+ @staticmethod
479
+ def cancel(state_value):
480
+ history = state_value["conversation_contexts"][
481
+ state_value["conversation_id"]
482
+ ]["history"]
483
+ history[-1]["loading"] = False
484
+ history[-1]["status"] = "done"
485
+ history[-1]["footer"] = "Chat completion paused"
486
+ return Events.postprocess_submit(state_value)
487
+
488
+ @staticmethod
489
+ def delete_message(state_value, e: gr.EventData):
490
+ index = e._data["payload"][0]["index"]
491
+ history = state_value["conversation_contexts"][
492
+ state_value["conversation_id"]
493
+ ]["history"]
494
+ history = history[:index] + history[index + 1 :]
495
+ state_value["conversation_contexts"][
496
+ state_value["conversation_id"]
497
+ ]["history"] = history
498
+ return gr.update(value=state_value)
499
+
500
+ @staticmethod
501
+ def edit_message(state_value, chatbot_value, e: gr.EventData):
502
+ index = e._data["payload"][0]["index"]
503
+ history = state_value["conversation_contexts"][
504
+ state_value["conversation_id"]
505
+ ]["history"]
506
+ history[index]["content"] = chatbot_value[index]["content"]
507
+ if not history[index].get("edited"):
508
+ history[index]["edited"] = True
509
+ footer = history[index].get("footer", "") or ""
510
+ history[index]["footer"] = (footer + " " if footer else "") + "Edited"
511
+ return gr.update(value=state_value), gr.update(value=history)
512
+
513
+ @staticmethod
514
+ def regenerate_message(state_value, e: gr.EventData):
515
+ index = e._data["payload"][0]["index"]
516
+ history = state_value["conversation_contexts"][
517
+ state_value["conversation_id"]
518
+ ]["history"]
519
+ history = history[:index]
520
+ state_value["conversation_contexts"][
521
+ state_value["conversation_id"]
522
+ ]["history"] = history
523
+
524
+ yield Events.preprocess_submit()(state_value)
525
+ try:
526
+ for chunk in Events.submit(state_value):
527
+ yield chunk
528
+ except Exception:
529
+ raise
530
+ finally:
531
+ yield Events.postprocess_submit(state_value)
532
+
533
+ @staticmethod
534
+ def apply_prompt(e: gr.EventData, input_value):
535
+ input_value["text"] = e._data["payload"][0]["value"]["description"]
536
+ urls = e._data["payload"][0]["value"].get("urls", [])
537
+ input_value["files"] = urls
538
+ return gr.update(value=input_value)
539
+
540
+ @staticmethod
541
+ def new_chat(state_value):
542
+ if not state_value["conversation_id"]:
543
+ return gr.skip()
544
+ state_value["conversation_id"] = ""
545
+ return (
546
+ gr.update(active_key=state_value["conversation_id"]),
547
+ gr.update(value=None),
548
+ gr.update(value=state_value),
549
+ )
550
+
551
+ @staticmethod
552
+ def select_conversation(state_value, e: gr.EventData):
553
+ active_key = e._data["payload"][0]
554
+ if state_value["conversation_id"] == active_key or (
555
+ active_key not in state_value["conversation_contexts"]
556
+ ):
557
+ return gr.skip()
558
+ state_value["conversation_id"] = active_key
559
+ return (
560
+ gr.update(active_key=active_key),
561
+ gr.update(
562
+ value=state_value["conversation_contexts"][active_key]["history"]
563
+ ),
564
+ gr.update(value=state_value),
565
+ )
566
+
567
+ @staticmethod
568
+ def click_conversation_menu(state_value, e: gr.EventData):
569
+ conversation_id = e._data["payload"][0]["key"]
570
+ operation = e._data["payload"][1]["key"]
571
+ if operation == "delete":
572
+ del state_value["conversation_contexts"][conversation_id]
573
+ state_value["conversations"] = [
574
+ item
575
+ for item in state_value["conversations"]
576
+ if item["key"] != conversation_id
577
+ ]
578
+ if state_value["conversation_id"] == conversation_id:
579
+ state_value["conversation_id"] = ""
580
+ return (
581
+ gr.update(
582
+ items=state_value["conversations"],
583
+ active_key=state_value["conversation_id"],
584
+ ),
585
+ gr.update(value=None),
586
+ gr.update(value=state_value),
587
+ )
588
+ return (
589
+ gr.update(items=state_value["conversations"]),
590
+ gr.skip(),
591
+ gr.update(value=state_value),
592
+ )
593
+
594
+ @staticmethod
595
+ def clear_conversation_history(state_value):
596
+ if not state_value["conversation_id"]:
597
+ return gr.skip()
598
+ state_value["conversation_contexts"][
599
+ state_value["conversation_id"]
600
+ ]["history"] = []
601
+ return gr.update(value=None), gr.update(value=state_value)
602
+
603
+ @staticmethod
604
+ def change_model(state_value, e: gr.EventData):
605
+ model_key = e._data["payload"][0]
606
+ state_value["model_key"] = model_key
607
+ return (
608
+ gr.update(
609
+ welcome_config=welcome_config(model_key),
610
+ bot_config=bot_config(model_key=model_key),
611
+ ),
612
+ gr.update(value=state_value),
613
+ )
614
+
615
+
616
+ # ---------------------------------------------------------------------------
617
+ # CSS
618
+ # ---------------------------------------------------------------------------
619
+
620
+ css = """
621
+ .gradio-container {
622
+ padding: 0 !important;
623
+ }
624
+ .gradio-container > main.fillable {
625
+ padding: 0 !important;
626
+ }
627
+ #chatbot {
628
+ height: calc(100vh - 21px - 16px - 36px);
629
+ max-height: 1500px;
630
+ }
631
+ #chatbot .chatbot-conversations {
632
+ height: 100vh;
633
+ background-color: var(--ms-gr-ant-color-bg-layout);
634
+ padding-left: 4px;
635
+ padding-right: 4px;
636
+ }
637
+ #chatbot .chatbot-conversations .chatbot-conversations-list {
638
+ padding-left: 0;
639
+ padding-right: 0;
640
+ }
641
+ #chatbot .chatbot-chat {
642
+ padding: 32px;
643
+ padding-bottom: 0;
644
+ height: 100%;
645
+ }
646
+ @media (max-width: 768px) {
647
+ #chatbot .chatbot-chat {
648
+ padding: 10px;
649
+ }
650
+ }
651
+ #chatbot .chatbot-chat .chatbot-chat-messages {
652
+ flex: 1;
653
+ }
654
+ .powered-by-bar {
655
+ display: flex;
656
+ align-items: center;
657
+ justify-content: space-between;
658
+ padding: 6px 16px;
659
+ border-bottom: 1px solid var(--ms-gr-ant-color-border-secondary, #f0f0f0);
660
+ }
661
+ .powered-by-bar .bar-title {
662
+ font-size: 24px;
663
+ font-weight: 600;
664
+ color: #333;
665
+ }
666
+ }
667
+ .powered-by-bar .bar-right {
668
+ display: flex;
669
+ align-items: center;
670
+ gap: 6px;
671
+ }
672
+ .powered-by-bar .bar-right span {
673
+ font-size: 11px;
674
+ color: #999;
675
+ text-transform: uppercase;
676
+ letter-spacing: 0.04em;
677
+ }
678
+ .powered-by-bar img {
679
+ height: 18px;
680
+ opacity: 0.85;
681
+ }
682
+ """
683
+
684
+ # ---------------------------------------------------------------------------
685
+ # Gradio UI
686
+ # ---------------------------------------------------------------------------
687
+
688
+ with gr.Blocks(css=css, fill_width=True) as demo:
689
+ state = gr.State(
690
+ {
691
+ "conversation_contexts": {},
692
+ "conversations": [],
693
+ "conversation_id": "",
694
+ "model_key": DEFAULT_MODEL_KEY,
695
+ }
696
+ )
697
+
698
+ with ms.Application(), antdx.XProvider(theme=DEFAULT_THEME), ms.AutoLoading():
699
+ # Top bar: title left, powered-by right
700
+ with ms.Div(elem_classes="powered-by-bar"):
701
+ ms.Span("Welcome to FW Vision Studio", elem_classes="bar-title")
702
+ with ms.Div(elem_classes="bar-right"):
703
+ ms.Span("Powered by")
704
+ antd.Image(
705
+ "./assets/fireworks_logo.png",
706
+ preview=False,
707
+ alt="Fireworks AI",
708
+ height=30,
709
+ )
710
+
711
+ with antd.Row(gutter=[20, 20], wrap=False, elem_id="chatbot"):
712
+ # --- Left sidebar ---
713
+ with antd.Col(
714
+ md=dict(flex="0 0 260px", span=24, order=0),
715
+ span=0,
716
+ order=1,
717
+ elem_style=dict(width=0),
718
+ ):
719
+ with ms.Div(elem_classes="chatbot-conversations"):
720
+ with antd.Flex(
721
+ vertical=True, gap="small", elem_style=dict(height="100%")
722
+ ):
723
+ # Model selector
724
+ model_select = antd.Select(
725
+ value=DEFAULT_MODEL_KEY,
726
+ options=[
727
+ {"label": m["label"], "value": k}
728
+ for k, m in MODELS.items()
729
+ ],
730
+ elem_style=dict(width="100%"),
731
+ )
732
+
733
+ # New conversation button
734
+ with antd.Button(
735
+ value=None, color="primary", variant="filled", block=True
736
+ ) as add_conversation_btn:
737
+ ms.Text("New Conversation")
738
+ with ms.Slot("icon"):
739
+ antd.Icon("PlusOutlined")
740
+
741
+ # Conversations list
742
+ with antdx.Conversations(
743
+ elem_classes="chatbot-conversations-list",
744
+ ) as conversations:
745
+ with ms.Slot("menu.items"):
746
+ with antd.Menu.Item(
747
+ label="Delete", key="delete", danger=True
748
+ ) as conversation_delete_menu_item:
749
+ with ms.Slot("icon"):
750
+ antd.Icon("DeleteOutlined")
751
+
752
+ # --- Right: chat area ---
753
+ with antd.Col(flex=1, elem_style=dict(height="100%")):
754
+ with antd.Flex(
755
+ vertical=True, gap="small", elem_classes="chatbot-chat"
756
+ ):
757
+ chatbot = pro.Chatbot(
758
+ elem_classes="chatbot-chat-messages",
759
+ height=0,
760
+ markdown_config=markdown_config(),
761
+ welcome_config=welcome_config(),
762
+ user_config=user_config(),
763
+ bot_config=bot_config(),
764
+ )
765
+
766
+ with pro.MultimodalInput(
767
+ placeholder="Ask me to analyze a document or convert a design to code...",
768
+ upload_config=upload_config(),
769
+ ) as input:
770
+ with ms.Slot("prefix"):
771
+ with antd.Flex(
772
+ gap=4,
773
+ wrap=True,
774
+ elem_style=dict(
775
+ maxWidth="40vw", display="inline-flex"
776
+ ),
777
+ ):
778
+ with antd.Button(value=None, type="text") as clear_btn:
779
+ with ms.Slot("icon"):
780
+ antd.Icon("ClearOutlined")
781
+
782
+ # --- Event wiring ---
783
+
784
+ # Model selector
785
+ model_select.change(
786
+ fn=Events.change_model,
787
+ inputs=[state],
788
+ outputs=[chatbot, state],
789
+ )
790
+
791
+ # Conversations
792
+ add_conversation_btn.click(
793
+ fn=Events.new_chat,
794
+ inputs=[state],
795
+ outputs=[conversations, chatbot, state],
796
+ )
797
+ conversations.active_change(
798
+ fn=Events.select_conversation,
799
+ inputs=[state],
800
+ outputs=[conversations, chatbot, state],
801
+ )
802
+ conversations.menu_click(
803
+ fn=Events.click_conversation_menu,
804
+ inputs=[state],
805
+ outputs=[conversations, chatbot, state],
806
+ )
807
+
808
+ # Chatbot
809
+ chatbot.welcome_prompt_select(
810
+ fn=Events.apply_prompt, inputs=[input], outputs=[input]
811
+ )
812
+ chatbot.delete(fn=Events.delete_message, inputs=[state], outputs=[state])
813
+ chatbot.edit(
814
+ fn=Events.edit_message,
815
+ inputs=[state, chatbot],
816
+ outputs=[state, chatbot],
817
+ )
818
+ regenerating_event = chatbot.retry(
819
+ fn=Events.regenerate_message,
820
+ inputs=[state],
821
+ outputs=[
822
+ input,
823
+ clear_btn,
824
+ conversation_delete_menu_item,
825
+ add_conversation_btn,
826
+ conversations,
827
+ chatbot,
828
+ state,
829
+ ],
830
+ )
831
+
832
+ # Input
833
+ submit_event = input.submit(
834
+ fn=Events.add_message,
835
+ inputs=[input, state],
836
+ outputs=[
837
+ input,
838
+ clear_btn,
839
+ conversation_delete_menu_item,
840
+ add_conversation_btn,
841
+ conversations,
842
+ chatbot,
843
+ state,
844
+ ],
845
+ )
846
+ input.cancel(
847
+ fn=Events.cancel,
848
+ inputs=[state],
849
+ outputs=[
850
+ input,
851
+ conversation_delete_menu_item,
852
+ clear_btn,
853
+ conversations,
854
+ add_conversation_btn,
855
+ chatbot,
856
+ state,
857
+ ],
858
+ cancels=[submit_event, regenerating_event],
859
+ queue=False,
860
+ )
861
+ clear_btn.click(
862
+ fn=Events.clear_conversation_history,
863
+ inputs=[state],
864
+ outputs=[chatbot, state],
865
+ )
866
+
867
+
868
+ if __name__ == "__main__":
869
+ demo.queue(default_concurrency_limit=100).launch(
870
+ server_name="0.0.0.0",
871
+ server_port=7860,
872
+ ssr_mode=False,
873
+ max_threads=100,
874
+ )
src/config.py ADDED
@@ -0,0 +1,287 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+
3
+ # Custom CSS with Fireworks AI purple palette
4
+ CUSTOM_CSS = """
5
+ /* Professional container with subtle purple gradient */
6
+ .gradio-container {
7
+ font-family: 'Inter', 'Segoe UI', system-ui, sans-serif;
8
+ background: linear-gradient(135deg, #FAFBFC 0%, #F3F0FF 100%);
9
+ }
10
+
11
+ /* Clean chat messages */
12
+ .chat-message {
13
+ border-radius: 12px;
14
+ padding: 16px;
15
+ margin: 8px 0;
16
+ background: white;
17
+ box-shadow: 0 1px 3px rgba(103, 32, 255, 0.08);
18
+ border: 1px solid #E6EAF4;
19
+ transition: all 0.2s ease;
20
+ }
21
+
22
+ .chat-message:hover {
23
+ box-shadow: 0 4px 12px rgba(103, 32, 255, 0.12);
24
+ }
25
+
26
+ /* Professional function calls with purple accent */
27
+ .function-call {
28
+ background: linear-gradient(to right, #F3F0FF, #FFFFFF);
29
+ border-left: 3px solid #6720FF;
30
+ padding: 16px;
31
+ margin: 12px 0;
32
+ border-radius: 8px;
33
+ box-shadow: 0 1px 2px rgba(103, 32, 255, 0.06);
34
+ font-family: 'JetBrains Mono', monospace;
35
+ font-size: 0.9em;
36
+ color: #0F172A;
37
+ }
38
+
39
+ /* Sleek meetings panel */
40
+ .meetings-container {
41
+ background: white;
42
+ border-radius: 16px;
43
+ padding: 24px;
44
+ box-shadow: 0 4px 6px rgba(103, 32, 255, 0.08);
45
+ border: 1px solid #E6EAF4;
46
+ }
47
+
48
+ /* Modern accordion with purple accents */
49
+ .meeting-details {
50
+ border: 1px solid #E6EAF4;
51
+ border-radius: 10px;
52
+ background: #FFFFFF;
53
+ margin: 10px 0;
54
+ transition: all 0.25s ease;
55
+ overflow: hidden;
56
+ }
57
+
58
+ .meeting-details:hover {
59
+ box-shadow: 0 4px 12px rgba(103, 32, 255, 0.1);
60
+ border-color: #C4B5FD;
61
+ }
62
+
63
+ .meeting-details[open] {
64
+ border-color: #6720FF;
65
+ box-shadow: 0 4px 12px rgba(103, 32, 255, 0.15);
66
+ }
67
+
68
+ /* Clean summary styling with purple */
69
+ .meeting-summary {
70
+ font-weight: 600;
71
+ color: #0F172A;
72
+ padding: 14px 18px;
73
+ cursor: pointer;
74
+ background: linear-gradient(90deg, transparent 0%, #F3F0FF 100%);
75
+ transition: background 0.2s ease;
76
+ }
77
+
78
+ .meeting-summary:hover {
79
+ background: linear-gradient(90deg, #F3F0FF 0%, #EDE9FE 100%);
80
+ }
81
+
82
+ /* Meeting content styling */
83
+ .meeting-content {
84
+ padding: 16px 18px;
85
+ color: #64748B;
86
+ border-top: 1px solid #E6EAF4;
87
+ background: #FAFBFC;
88
+ }
89
+
90
+ /* Professional content cards */
91
+ .content-card {
92
+ background: white;
93
+ border-radius: 16px;
94
+ padding: 28px;
95
+ box-shadow: 0 4px 6px rgba(103, 32, 255, 0.06);
96
+ border: 1px solid #E6EAF4;
97
+ }
98
+
99
+ /* Purple accent links */
100
+ a {
101
+ color: #6720FF;
102
+ text-decoration: none;
103
+ transition: color 0.2s ease;
104
+ }
105
+
106
+ a:hover {
107
+ color: #7B2FFF;
108
+ text-decoration: underline;
109
+ }
110
+
111
+ /* Custom scrollbar with purple */
112
+ ::-webkit-scrollbar {
113
+ width: 8px;
114
+ height: 8px;
115
+ }
116
+
117
+ ::-webkit-scrollbar-track {
118
+ background: #F3F0FF;
119
+ border-radius: 4px;
120
+ }
121
+
122
+ ::-webkit-scrollbar-thumb {
123
+ background: #C4B5FD;
124
+ border-radius: 4px;
125
+ }
126
+
127
+ ::-webkit-scrollbar-thumb:hover {
128
+ background: #A78BFA;
129
+ }
130
+
131
+ /* Search box styling */
132
+ .search-box {
133
+ border: 2px solid #E6EAF4;
134
+ border-radius: 10px;
135
+ transition: all 0.2s ease;
136
+ }
137
+
138
+ .search-box:focus {
139
+ border-color: #6720FF;
140
+ box-shadow: 0 0 0 3px rgba(103, 32, 255, 0.1);
141
+ }
142
+
143
+ /* Header styling */
144
+ .header-title {
145
+ background: linear-gradient(135deg, #6720FF 0%, #8B5CF6 100%);
146
+ -webkit-background-clip: text;
147
+ -webkit-text-fill-color: transparent;
148
+ background-clip: text;
149
+ font-weight: 700;
150
+ }
151
+
152
+ /* Chat window styling - readable font size */
153
+ .chat-window {
154
+ font-size: 0.95em !important;
155
+ }
156
+
157
+ .chat-window .message {
158
+ font-size: 0.95em !important;
159
+ }
160
+
161
+ .chat-window .message-content {
162
+ font-size: 0.95em !important;
163
+ line-height: 1.6;
164
+ }
165
+
166
+ /* Readable text in chat messages */
167
+ .chat-window p {
168
+ font-size: 0.95em !important;
169
+ }
170
+
171
+ .chat-window code {
172
+ font-size: 0.9em !important;
173
+ }
174
+
175
+ .chat-window pre {
176
+ font-size: 0.9em !important;
177
+ }
178
+
179
+ /* Slightly smaller tool calling cards */
180
+ .chat-window .tool-call {
181
+ font-size: 0.92em !important;
182
+ }
183
+
184
+ .chat-window table {
185
+ font-size: 0.92em !important;
186
+ }
187
+
188
+ .chat-window th,
189
+ .chat-window td {
190
+ font-size: 0.92em !important;
191
+ padding: 6px 10px !important;
192
+ }
193
+
194
+ /* Proportional headings in chat */
195
+ .chat-window h1 {
196
+ font-size: 1.5em !important;
197
+ }
198
+
199
+ .chat-window h2 {
200
+ font-size: 1.3em !important;
201
+ }
202
+
203
+ .chat-window h3 {
204
+ font-size: 1.15em !important;
205
+ }
206
+
207
+ .chat-window h4,
208
+ .chat-window h5,
209
+ .chat-window h6 {
210
+ font-size: 1em !important;
211
+ }
212
+
213
+ /* List items */
214
+ .chat-window ul,
215
+ .chat-window ol {
216
+ font-size: 0.95em !important;
217
+ }
218
+
219
+ .chat-window li {
220
+ font-size: 0.95em !important;
221
+ }
222
+
223
+ /* Compact header spacing */
224
+ .compact-header {
225
+ margin-bottom: 8px !important;
226
+ padding: 8px 0 !important;
227
+ }
228
+
229
+ .compact-input {
230
+ margin-bottom: 4px !important;
231
+ }
232
+
233
+ .compact-input label {
234
+ font-size: 0.9em !important;
235
+ margin-bottom: 4px !important;
236
+ }
237
+
238
+ /* Compact example questions */
239
+ .compact-examples {
240
+ margin: 8px 0 12px 0 !important;
241
+ gap: 6px !important;
242
+ }
243
+
244
+ .compact-examples button {
245
+ font-size: 0.85em !important;
246
+ padding: 6px 10px !important;
247
+ min-height: 32px !important;
248
+ }
249
+ """
250
+
251
+ # Create custom Fireworks AI-inspired theme
252
+ GRADIO_THEME = gr.themes.Base(
253
+ primary_hue=gr.themes.colors.purple,
254
+ secondary_hue=gr.themes.colors.violet,
255
+ neutral_hue=gr.themes.colors.slate,
256
+ spacing_size=gr.themes.sizes.spacing_lg,
257
+ radius_size=gr.themes.sizes.radius_md,
258
+ text_size=gr.themes.sizes.text_md,
259
+ font=[gr.themes.GoogleFont('Inter'), 'ui-sans-serif', 'system-ui', 'sans-serif'],
260
+ font_mono=[gr.themes.GoogleFont('JetBrains Mono'), 'monospace']
261
+ ).set(
262
+ # Primary button styling (Fireworks AI purple)
263
+ button_primary_background_fill='#6720FF',
264
+ button_primary_background_fill_hover='#7B2FFF',
265
+ button_primary_text_color='#FFFFFF',
266
+
267
+ # Secondary button styling
268
+ button_secondary_background_fill='#F3F0FF',
269
+ button_secondary_background_fill_hover='#EDE9FE',
270
+ button_secondary_text_color='#6720FF',
271
+
272
+ # Interactive elements
273
+ slider_color='#6720FF',
274
+
275
+ # Links
276
+ link_text_color='#6720FF',
277
+ link_text_color_hover='#7B2FFF',
278
+ link_text_color_visited='#8B5CF6',
279
+
280
+ # Backgrounds
281
+ body_background_fill='#FAFBFC',
282
+ block_background_fill='#FFFFFF',
283
+ input_background_fill='#FFFFFF',
284
+
285
+ # Borders
286
+ border_color_primary='#E6EAF4',
287
+ )