visualknowledge 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/frontend/index.html +41 -0
- package/frontend/src/App.jsx +59 -0
- package/frontend/src/components/AssistantMessage.jsx +58 -0
- package/frontend/src/components/ChatArea.jsx +20 -0
- package/frontend/src/components/InputArea.jsx +68 -0
- package/frontend/src/components/Message.jsx +16 -0
- package/frontend/src/components/MessageList.jsx +23 -0
- package/frontend/src/components/TextPart.jsx +45 -0
- package/frontend/src/components/TopBar.jsx +27 -0
- package/frontend/src/components/TypingIndicator.jsx +13 -0
- package/frontend/src/components/UserMessage.jsx +14 -0
- package/frontend/src/components/WelcomeScreen.jsx +15 -0
- package/frontend/src/components/widgets/FullscreenOverlay.jsx +44 -0
- package/frontend/src/components/widgets/HtmlWidget.jsx +89 -0
- package/frontend/src/components/widgets/MermaidWidget.jsx +61 -0
- package/frontend/src/components/widgets/SvgWidget.jsx +59 -0
- package/frontend/src/components/widgets/WidgetContainer.jsx +32 -0
- package/frontend/src/context/AppContext.jsx +151 -0
- package/frontend/src/hooks/useChat.js +160 -0
- package/frontend/src/hooks/useModels.js +52 -0
- package/frontend/src/hooks/useTheme.js +31 -0
- package/frontend/src/lib/markdownRenderer.js +80 -0
- package/frontend/src/lib/mermaidRenderer.js +99 -0
- package/frontend/src/lib/streamProcessor.js +205 -0
- package/frontend/src/main.js +16 -0
- package/frontend/src/utils/escape.js +10 -0
- package/frontend/styles/base.css +7 -0
- package/frontend/styles/chat-area.css +24 -0
- package/frontend/styles/input-area.css +37 -0
- package/frontend/styles/markdown.css +50 -0
- package/frontend/styles/message.css +24 -0
- package/frontend/styles/top-bar.css +32 -0
- package/frontend/styles/variables.css +74 -0
- package/frontend/styles/widget.css +62 -0
- package/package.json +29 -1
- package/server.py +11 -1
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StreamProcessor - 纯函数式流式文本解析器
|
|
3
|
+
*
|
|
4
|
+
* 将 SSE 流式文本按代码块边界(```mermaid / ```html / ```svg)拆分为 ContentSegment 列表。
|
|
5
|
+
* 完全无 DOM 依赖,可在任何环境运行和测试。
|
|
6
|
+
*
|
|
7
|
+
* State Machine:
|
|
8
|
+
* text ──[检测到 ```lang]──> codeblock ──[检测到 ``` 关闭]──> text
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
let _uidCounter = 0;
|
|
12
|
+
function _nextId(prefix = 'seg') {
|
|
13
|
+
return `${prefix}_${Date.now()}_${++_uidCounter}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class StreamProcessor {
|
|
17
|
+
constructor() {
|
|
18
|
+
this.reset();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 重置状态机(用于开始处理新一条消息)
|
|
23
|
+
*/
|
|
24
|
+
reset() {
|
|
25
|
+
this._phase = 'text'; // 'text' | 'codeblock'
|
|
26
|
+
this._currentLang = ''; // '' | 'mermaid' | 'html' | 'svg'
|
|
27
|
+
this._buffer = ''; // 当前阶段累积的文本缓冲区
|
|
28
|
+
this._tickCount = 0; // 反引号计数器
|
|
29
|
+
this._segments = []; // 已完成的段列表
|
|
30
|
+
this._activeText = ''; // 当前活跃的文字段内容
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 输入新的文本块
|
|
35
|
+
* @param {string} chunk - 从 SSE 接收的新文本片段
|
|
36
|
+
*/
|
|
37
|
+
feed(chunk) {
|
|
38
|
+
// 过滤 [VISUALIZE:] 标签
|
|
39
|
+
chunk = chunk.replace(/^\[VISUALIZE:\s*(yes|no)\]\s*\n?/m, '');
|
|
40
|
+
|
|
41
|
+
if (this._phase === 'text') {
|
|
42
|
+
this._activeText += chunk;
|
|
43
|
+
|
|
44
|
+
// 检测代码块起始
|
|
45
|
+
const hit = this._detectBlockStart(chunk);
|
|
46
|
+
if (hit) {
|
|
47
|
+
this._phase = 'codeblock';
|
|
48
|
+
this._currentLang = hit.lang;
|
|
49
|
+
this._buffer = '';
|
|
50
|
+
this._tickCount = 0;
|
|
51
|
+
|
|
52
|
+
// 冻结当前文字为 text segment
|
|
53
|
+
const before = this._activeText.substring(0, hit.absIdx);
|
|
54
|
+
if (before.trim()) {
|
|
55
|
+
this._segments.push({
|
|
56
|
+
id: _nextId('txt'),
|
|
57
|
+
type: 'text',
|
|
58
|
+
content: before,
|
|
59
|
+
renderedContent: null,
|
|
60
|
+
renderStatus: 'pending',
|
|
61
|
+
renderError: null,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 创建新的 codeblock segment 占位(content 将在 codeblock 阶段累积)
|
|
66
|
+
this._segments.push({
|
|
67
|
+
id: _nextId(this._currentLang),
|
|
68
|
+
type: this._currentLang,
|
|
69
|
+
content: '',
|
|
70
|
+
renderedContent: null,
|
|
71
|
+
renderStatus: 'pending',
|
|
72
|
+
renderError: null,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// 重置文字累积
|
|
76
|
+
this._activeText = '';
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 仍在 text 阶段,无需特殊处理(调用方会 getSegments)
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// === codeblock 阶段 ===
|
|
85
|
+
for (let i = 0; i < chunk.length; i++) {
|
|
86
|
+
const ch = chunk[i];
|
|
87
|
+
if (ch === '`') {
|
|
88
|
+
this._tickCount++;
|
|
89
|
+
} else {
|
|
90
|
+
if (this._tickCount >= 3) {
|
|
91
|
+
// 检测到代码块关闭
|
|
92
|
+
this._phase = 'text';
|
|
93
|
+
this._tickCount = 0;
|
|
94
|
+
|
|
95
|
+
// 更新当前 codeblock segment 的 content
|
|
96
|
+
const codeSeg = this._segments[this._segments.length - 1];
|
|
97
|
+
if (codeSeg) {
|
|
98
|
+
codeSeg.content = this._buffer;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
this._buffer = '';
|
|
102
|
+
this._currentLang = '';
|
|
103
|
+
|
|
104
|
+
// 开始新的文字累积
|
|
105
|
+
this._activeText = '';
|
|
106
|
+
continue; // 跳过当前字符(它是关闭 ``` 的一部分)
|
|
107
|
+
}
|
|
108
|
+
if (this._tickCount > 0) {
|
|
109
|
+
this._buffer += '`'.repeat(this._tickCount);
|
|
110
|
+
this._tickCount = 0;
|
|
111
|
+
}
|
|
112
|
+
this._buffer += ch;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* 获取当前所有已解析的段列表
|
|
119
|
+
* 包含:
|
|
120
|
+
* - 已完成冻结的 segments
|
|
121
|
+
* - 当前活跃的 text 段(如果有的话,作为最后一个 segment 返回)
|
|
122
|
+
* - 当前活跃的 codeblock 段(如果在 codeblock 阶段)
|
|
123
|
+
* @returns {Array} ContentSegment[]
|
|
124
|
+
*/
|
|
125
|
+
getSegments() {
|
|
126
|
+
// 返回 segments 的快照,包含当前活跃段
|
|
127
|
+
const result = [...this._segments];
|
|
128
|
+
|
|
129
|
+
// 如果有活跃的文字段且不在 segments 中,追加它
|
|
130
|
+
if (this._phase === 'text' && this._activeText) {
|
|
131
|
+
// 检查是否已有未完成的 text segment(最后一个 segment 可能是待更新的 text)
|
|
132
|
+
const lastSeg = result[result.length - 1];
|
|
133
|
+
if (lastSeg && lastSeg.type === 'text' && lastSeg.renderStatus === 'pending' && !lastSeg.content) {
|
|
134
|
+
// 更新已有的空 text segment
|
|
135
|
+
lastSeg.content = this._activeText;
|
|
136
|
+
} else if (this._activeText.trim()) {
|
|
137
|
+
result.push({
|
|
138
|
+
id: _nextId('txt_live'),
|
|
139
|
+
type: 'text',
|
|
140
|
+
content: this._activeText,
|
|
141
|
+
renderedContent: null,
|
|
142
|
+
renderStatus: 'pending',
|
|
143
|
+
renderError: null,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return result;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* 结束流式解析,返回最终段列表
|
|
153
|
+
* 处理未闭合的 codeblock 等边缘情况
|
|
154
|
+
* @returns {Array} ContentSegment[]
|
|
155
|
+
*/
|
|
156
|
+
finalize() {
|
|
157
|
+
// 如果仍在 codeblock 阶段,强制结束
|
|
158
|
+
if (this._phase === 'codeblock') {
|
|
159
|
+
const codeSeg = this._segments[this._segments.length - 1];
|
|
160
|
+
if (codeSeg) {
|
|
161
|
+
codeSeg.content = this._buffer;
|
|
162
|
+
}
|
|
163
|
+
this._phase = 'text';
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// 收集最终文字段
|
|
167
|
+
if (this._activeText.trim()) {
|
|
168
|
+
// 去重:检查最后是否已有相同内容的 text segment
|
|
169
|
+
const lastSeg = this._segments[this._segments.length - 1];
|
|
170
|
+
if (!(lastSeg && lastSeg.type === 'text' && lastSeg.content === this._activeText)) {
|
|
171
|
+
this._segments.push({
|
|
172
|
+
id: _nextId('txt_final'),
|
|
173
|
+
type: 'text',
|
|
174
|
+
content: this._activeText,
|
|
175
|
+
renderedContent: null,
|
|
176
|
+
renderStatus: 'pending',
|
|
177
|
+
renderError: null,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return this._segments;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* 在最新文本中检测代码块起始标记
|
|
187
|
+
* @param {string} newChunk - 最新接收到的文本块
|
|
188
|
+
* @returns {{ lang: string, absIdx: number } | null}
|
|
189
|
+
* @private
|
|
190
|
+
*/
|
|
191
|
+
_detectBlockStart(newChunk) {
|
|
192
|
+
const lookback = this._activeText.substring(
|
|
193
|
+
Math.max(0, this._activeText.length - 30)
|
|
194
|
+
);
|
|
195
|
+
for (const lang of ['mermaid', 'html', 'svg']) {
|
|
196
|
+
const marker = '```' + lang;
|
|
197
|
+
const idx = lookback.lastIndexOf(marker);
|
|
198
|
+
if (idx !== -1) {
|
|
199
|
+
const absIdx = this._activeText.length - lookback.length + idx;
|
|
200
|
+
return { lang, absIdx };
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Application Entry Point
|
|
3
|
+
*
|
|
4
|
+
* 使用 React 18 createRoot API 挂载根组件。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React from 'react';
|
|
8
|
+
import { createRoot } from 'react-dom/client';
|
|
9
|
+
import { App } from './App.jsx';
|
|
10
|
+
|
|
11
|
+
const rootEl = document.getElementById('root');
|
|
12
|
+
if (!rootEl) {
|
|
13
|
+
throw new Error('Root element #root not found in DOM');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
createRoot(rootEl).render(React.createElement(App));
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/* Reset & Base Layout */
|
|
2
|
+
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
|
3
|
+
html,body{height:100%;overflow:hidden}
|
|
4
|
+
body{
|
|
5
|
+
font-family:var(--font-sans);background:var(--bg-primary);color:var(--text-primary);
|
|
6
|
+
display:flex;flex-direction:column;
|
|
7
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/* Chat Area */
|
|
2
|
+
.chat-area{
|
|
3
|
+
flex:1;overflow-y:auto;padding:0;display:flex;flex-direction:column;
|
|
4
|
+
scroll-behavior:smooth;
|
|
5
|
+
}
|
|
6
|
+
.chat-area::-webkit-scrollbar{width:6px}
|
|
7
|
+
.chat-area::-webkit-scrollbar-track{background:transparent}
|
|
8
|
+
.chat-area::-webkit-scrollbar-thumb{background:#ffffff15;border-radius:3px}
|
|
9
|
+
.chat-area::-webkit-scrollbar-thumb:hover{background:#ffffff25}
|
|
10
|
+
|
|
11
|
+
.messages{max-width:768px;width:100%;margin:0 auto;padding:16px 24px;flex:1}
|
|
12
|
+
|
|
13
|
+
.welcome{
|
|
14
|
+
display:flex;flex-direction:column;align-items:center;justify-content:center;
|
|
15
|
+
height:100%;gap:16px;opacity:0.7;text-align:center;padding:40px;
|
|
16
|
+
}
|
|
17
|
+
.welcome .icon{
|
|
18
|
+
width:64px;height:64px;border-radius:20px;
|
|
19
|
+
background:linear-gradient(135deg,#d97706,#f59e0b);
|
|
20
|
+
display:flex;align-items:center;justify-content:center;
|
|
21
|
+
font-size:28px;color:#fff;
|
|
22
|
+
}
|
|
23
|
+
.welcome h2{font-size:22px;font-weight:600;color:var(--text-primary)}
|
|
24
|
+
.welcome p{color:var(--text-muted);font-size:14px;line-height:1.6;max-width:400px}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/* Input Area */
|
|
2
|
+
.input-area{
|
|
3
|
+
padding:12px 24px 20px;flex-shrink:0;
|
|
4
|
+
background:linear-gradient(to top,var(--bg-primary) 60%,transparent);
|
|
5
|
+
}
|
|
6
|
+
.input-wrapper{
|
|
7
|
+
max-width:768px;margin:0 auto;position:relative;
|
|
8
|
+
background:var(--bg-input);border:1px solid var(--border-light);
|
|
9
|
+
border-radius:var(--radius-xl);transition:border-color 0.2s;
|
|
10
|
+
}
|
|
11
|
+
.input-wrapper:focus-within{border-color:var(--accent)}
|
|
12
|
+
.input-wrapper textarea{
|
|
13
|
+
width:100%;background:none;border:none;color:var(--text-primary);
|
|
14
|
+
padding:14px 56px 14px 18px;font-size:15px;line-height:1.6;
|
|
15
|
+
resize:none;outline:none;min-height:24px;max-height:200px;
|
|
16
|
+
font-family:var(--font-sans);
|
|
17
|
+
}
|
|
18
|
+
.input-wrapper textarea::placeholder{color:var(--text-faint)}
|
|
19
|
+
.send-btn{
|
|
20
|
+
position:absolute;right:10px;bottom:8px;width:36px;height:36px;
|
|
21
|
+
border-radius:10px;border:none;cursor:pointer;
|
|
22
|
+
background:var(--accent);color:#fff;
|
|
23
|
+
display:flex;align-items:center;justify-content:center;
|
|
24
|
+
transition:all 0.2s;opacity:0.4;
|
|
25
|
+
}
|
|
26
|
+
.send-btn.active{opacity:1}
|
|
27
|
+
.send-btn:hover{transform:scale(1.05);background:var(--accent-hover)}
|
|
28
|
+
.send-btn svg{width:18px;height:18px}
|
|
29
|
+
|
|
30
|
+
.typing-indicator{display:flex;align-items:center;gap:4px;padding:4px 0}
|
|
31
|
+
.typing-indicator span{
|
|
32
|
+
width:6px;height:6px;border-radius:50%;background:var(--text-muted);
|
|
33
|
+
animation:blink 1.4s infinite;
|
|
34
|
+
}
|
|
35
|
+
.typing-indicator span:nth-child(2){animation-delay:0.2s}
|
|
36
|
+
.typing-indicator span:nth-child(3){animation-delay:0.4s}
|
|
37
|
+
@keyframes blink{0%,60%,100%{opacity:0.3}30%{opacity:1}}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/* Markdown Rendering */
|
|
2
|
+
.message.assistant .content strong{color:var(--text-primary);font-weight:600}
|
|
3
|
+
.message.assistant .content em{color:var(--text-secondary);font-style:italic}
|
|
4
|
+
.message.assistant .content code{
|
|
5
|
+
background:var(--blockquote-bg);border:1px solid var(--border-light);
|
|
6
|
+
padding:1px 6px;border-radius:4px;font-size:13px;
|
|
7
|
+
font-family:var(--font-mono);color:var(--color-yellow);
|
|
8
|
+
}
|
|
9
|
+
.message.assistant .content pre{
|
|
10
|
+
background:var(--code-bg);border:1px solid var(--code-border);
|
|
11
|
+
border-radius:var(--radius);padding:14px 18px;margin:12px 0;
|
|
12
|
+
overflow-x:auto;
|
|
13
|
+
}
|
|
14
|
+
.message.assistant .content pre code{
|
|
15
|
+
background:none;border:none;padding:0;color:var(--text-secondary);font-size:13px;
|
|
16
|
+
}
|
|
17
|
+
.message.assistant .content ul,.message.assistant .content ol{
|
|
18
|
+
margin:8px 0 8px 24px;
|
|
19
|
+
}
|
|
20
|
+
.message.assistant .content li{margin-bottom:4px;line-height:1.7}
|
|
21
|
+
.message.assistant .content h1,.message.assistant .content h2,.message.assistant .content h3{
|
|
22
|
+
color:var(--text-primary);font-weight:700;margin:20px 0 8px;
|
|
23
|
+
}
|
|
24
|
+
.message.assistant .content h2{font-size:17px}
|
|
25
|
+
.message.assistant .content h3{font-size:16px}
|
|
26
|
+
.message.assistant .content hr{
|
|
27
|
+
border:none;border-top:1px solid var(--border-light);margin:16px 0;
|
|
28
|
+
}
|
|
29
|
+
.message.assistant .content blockquote{
|
|
30
|
+
border-left:3px solid var(--accent);padding:4px 14px;margin:10px 0;
|
|
31
|
+
color:var(--text-muted);background:var(--blockquote-bg);border-radius:0 8px 8px 0;
|
|
32
|
+
}
|
|
33
|
+
.message.assistant .content table{
|
|
34
|
+
border-collapse:collapse;margin:12px 0;width:100%;
|
|
35
|
+
}
|
|
36
|
+
.message.assistant .content th,.message.assistant .content td{
|
|
37
|
+
border:1px solid var(--border-light);padding:8px 12px;text-align:left;
|
|
38
|
+
}
|
|
39
|
+
.message.assistant .content th{
|
|
40
|
+
background:var(--table-header-bg);font-weight:600;color:var(--text-primary);
|
|
41
|
+
}
|
|
42
|
+
.message.assistant .content tr:nth-child(even){background:var(--table-even-bg)}
|
|
43
|
+
.message.assistant .content a{color:var(--color-blue);text-decoration:none}
|
|
44
|
+
.message.assistant .content a:hover{text-decoration:underline}
|
|
45
|
+
.message.assistant .content img{max-width:100%;border-radius:var(--radius)}
|
|
46
|
+
|
|
47
|
+
/* KaTeX 数学公式主题适配 */
|
|
48
|
+
.message.assistant .content .katex-display{margin:14px 0;overflow-x:auto}
|
|
49
|
+
.message.assistant .content .katex{color:var(--text-primary);font-size:1em}
|
|
50
|
+
[data-theme="light"] .message.assistant .content .katex{color:#1c1917}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/* Message Bubbles */
|
|
2
|
+
.message{margin-bottom:24px;animation:fadeIn 0.3s ease}
|
|
3
|
+
@keyframes fadeIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
|
|
4
|
+
|
|
5
|
+
.message.user .bubble{
|
|
6
|
+
background:var(--bg-user-msg);border:1px solid var(--border-light);
|
|
7
|
+
border-radius:var(--radius-xl) var(--radius-xl) 4px var(--radius-xl);
|
|
8
|
+
padding:12px 18px;margin-left:48px;max-width:85%;
|
|
9
|
+
font-size:15px;line-height:1.7;white-space:pre-wrap;word-break:break-word;
|
|
10
|
+
color:var(--text-primary);
|
|
11
|
+
}
|
|
12
|
+
.message.assistant .avatar{
|
|
13
|
+
width:28px;height:28px;border-radius:8px;
|
|
14
|
+
background:linear-gradient(135deg,#d97706,#f59e0b);
|
|
15
|
+
display:flex;align-items:center;justify-content:center;
|
|
16
|
+
font-weight:700;font-size:12px;color:#fff;margin-bottom:8px;
|
|
17
|
+
}
|
|
18
|
+
.message.assistant .content{
|
|
19
|
+
font-size:15px;line-height:1.8;color:var(--text-primary);
|
|
20
|
+
padding-left:2px;
|
|
21
|
+
}
|
|
22
|
+
.message.assistant .content p{margin-bottom:12px}
|
|
23
|
+
.message.assistant .content p:last-child{margin-bottom:0}
|
|
24
|
+
.message.assistant .content .text-part{margin-bottom:4px}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/* Top Bar */
|
|
2
|
+
.top-bar{
|
|
3
|
+
height:48px;display:flex;align-items:center;justify-content:space-between;
|
|
4
|
+
padding:0 16px;border-bottom:1px solid var(--border-light);flex-shrink:0;
|
|
5
|
+
background:var(--bg-primary);
|
|
6
|
+
}
|
|
7
|
+
.top-bar .left{display:flex;align-items:center;gap:10px}
|
|
8
|
+
.top-bar .logo{
|
|
9
|
+
width:28px;height:28px;border-radius:8px;
|
|
10
|
+
background:linear-gradient(135deg,#d97706,#f59e0b);
|
|
11
|
+
display:flex;align-items:center;justify-content:center;
|
|
12
|
+
font-weight:700;font-size:14px;color:#fff;
|
|
13
|
+
}
|
|
14
|
+
.top-bar h1{font-size:15px;font-weight:600;letter-spacing:-0.3px;color:var(--text-primary)}
|
|
15
|
+
.top-bar .model-select{
|
|
16
|
+
background:var(--bg-tertiary);border:1px solid var(--border-light);
|
|
17
|
+
color:var(--text-secondary);padding:4px 10px;border-radius:8px;
|
|
18
|
+
font-size:12px;cursor:pointer;outline:none;
|
|
19
|
+
}
|
|
20
|
+
.top-bar .model-select:hover{border-color:var(--accent)}
|
|
21
|
+
|
|
22
|
+
.theme-toggle{
|
|
23
|
+
background:var(--bg-tertiary);border:1px solid var(--border-light);
|
|
24
|
+
color:var(--text-muted);width:34px;height:34px;border-radius:8px;
|
|
25
|
+
display:flex;align-items:center;justify-content:center;cursor:pointer;
|
|
26
|
+
transition:all 0.2s;
|
|
27
|
+
}
|
|
28
|
+
.theme-toggle:hover{border-color:var(--accent);color:var(--accent)}
|
|
29
|
+
.theme-toggle .icon-sun{display:none}
|
|
30
|
+
.theme-toggle .icon-moon{display:block}
|
|
31
|
+
[data-theme="light"] .theme-toggle .icon-sun{display:block}
|
|
32
|
+
[data-theme="light"] .theme-toggle .icon-moon{display:none}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/* CSS Variables & Theme Definitions */
|
|
2
|
+
:root{
|
|
3
|
+
--bg-primary:#1c1917;
|
|
4
|
+
--bg-secondary:#1c1917;
|
|
5
|
+
--bg-tertiary:#292524;
|
|
6
|
+
--bg-input:#292524;
|
|
7
|
+
--bg-hover:#3f3a36;
|
|
8
|
+
--bg-user-msg:#2a2520;
|
|
9
|
+
--text-primary:#fafaf9;
|
|
10
|
+
--text-secondary:#d6d3d1;
|
|
11
|
+
--text-muted:#a8a29e;
|
|
12
|
+
--text-faint:#78716c;
|
|
13
|
+
--accent:#d97706;
|
|
14
|
+
--accent-soft:#d9770620;
|
|
15
|
+
--accent-hover:#f59e0b;
|
|
16
|
+
--border:#ffffff0a;
|
|
17
|
+
--border-light:#ffffff12;
|
|
18
|
+
--border-medium:#ffffff1a;
|
|
19
|
+
--radius:12px;
|
|
20
|
+
--radius-lg:16px;
|
|
21
|
+
--radius-xl:20px;
|
|
22
|
+
--shadow:0 4px 24px rgba(0,0,0,0.4);
|
|
23
|
+
--font-sans:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
|
|
24
|
+
--font-mono:'SF Mono',CaskaydiaCove Nerd Font,'Cascadia Code',Consolas,monospace;
|
|
25
|
+
--color-amber:#f59e0b;
|
|
26
|
+
--color-blue:#60a5fa;
|
|
27
|
+
--color-purple:#a78bfa;
|
|
28
|
+
--color-green:#4ade80;
|
|
29
|
+
--color-rose:#fb7185;
|
|
30
|
+
--color-teal:#2dd4bf;
|
|
31
|
+
--color-orange:#fb923c;
|
|
32
|
+
--color-yellow:#fbbf24;
|
|
33
|
+
--code-bg:#0c0a09;
|
|
34
|
+
--code-border:#ffffff12;
|
|
35
|
+
--blockquote-bg:#ffffff03;
|
|
36
|
+
--table-header-bg:#292524;
|
|
37
|
+
--table-even-bg:#ffffff03;
|
|
38
|
+
--widget-bg:#1c1917;
|
|
39
|
+
--widget-header-bg:#292524;
|
|
40
|
+
}
|
|
41
|
+
[data-theme="light"]{
|
|
42
|
+
--bg-primary:#fafaf9;
|
|
43
|
+
--bg-secondary:#fafaf9;
|
|
44
|
+
--bg-tertiary:#f5f5f4;
|
|
45
|
+
--bg-input:#f5f5f4;
|
|
46
|
+
--bg-hover:#e7e5e4;
|
|
47
|
+
--bg-user-msg:#f0ede8;
|
|
48
|
+
--text-primary:#1c1917;
|
|
49
|
+
--text-secondary:#44403c;
|
|
50
|
+
--text-muted:#78716c;
|
|
51
|
+
--text-faint:#a8a29e;
|
|
52
|
+
--accent:#d97706;
|
|
53
|
+
--accent-soft:#d9770615;
|
|
54
|
+
--accent-hover:#b45309;
|
|
55
|
+
--border:#00000008;
|
|
56
|
+
--border-light:#0000000d;
|
|
57
|
+
--border-medium:#00000015;
|
|
58
|
+
--shadow:0 4px 24px rgba(0,0,0,0.08);
|
|
59
|
+
--color-amber:#d97706;
|
|
60
|
+
--color-blue:#2563eb;
|
|
61
|
+
--color-purple:#7c3aed;
|
|
62
|
+
--color-green:#16a34a;
|
|
63
|
+
--color-rose:#e11d48;
|
|
64
|
+
--color-teal:#0d9488;
|
|
65
|
+
--color-orange:#ea580c;
|
|
66
|
+
--color-yellow:#ca8a04;
|
|
67
|
+
--code-bg:#f5f5f4;
|
|
68
|
+
--code-border:#e7e5e4;
|
|
69
|
+
--blockquote-bg:#00000003;
|
|
70
|
+
--table-header-bg:#f5f5f4;
|
|
71
|
+
--table-even-bg:#00000002;
|
|
72
|
+
--widget-bg:#ffffff;
|
|
73
|
+
--widget-header-bg:#f5f5f4;
|
|
74
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/* Widget Containers & Fullscreen Overlay */
|
|
2
|
+
.widget-container{
|
|
3
|
+
margin:14px 0;border:1px solid var(--border-light);
|
|
4
|
+
border-radius:var(--radius-lg);overflow:hidden;
|
|
5
|
+
background:var(--widget-bg);
|
|
6
|
+
}
|
|
7
|
+
.widget-container .widget-header{
|
|
8
|
+
display:flex;align-items:center;justify-content:space-between;
|
|
9
|
+
padding:10px 16px;border-bottom:1px solid var(--border);
|
|
10
|
+
background:var(--widget-header-bg);
|
|
11
|
+
}
|
|
12
|
+
.widget-container .widget-header .left{display:flex;align-items:center;gap:8px}
|
|
13
|
+
.widget-container .widget-header .badge{
|
|
14
|
+
background:var(--accent-soft);color:var(--accent);
|
|
15
|
+
padding:2px 8px;border-radius:6px;font-size:11px;font-weight:600;
|
|
16
|
+
}
|
|
17
|
+
.widget-container .widget-header .widget-type{font-size:13px;color:var(--text-muted)}
|
|
18
|
+
.widget-container .widget-header .btn{
|
|
19
|
+
background:none;border:1px solid var(--border-light);color:var(--text-muted);
|
|
20
|
+
padding:4px 10px;border-radius:6px;font-size:11px;cursor:pointer;
|
|
21
|
+
transition:all 0.2s;
|
|
22
|
+
}
|
|
23
|
+
.widget-container .widget-header .btn:hover{border-color:var(--accent);color:var(--accent)}
|
|
24
|
+
.widget-container .widget-body{
|
|
25
|
+
padding:0;display:flex;justify-content:center;overflow:auto;
|
|
26
|
+
min-height:100px;
|
|
27
|
+
}
|
|
28
|
+
.widget-container .widget-body svg{max-width:100%;height:auto}
|
|
29
|
+
.widget-container .widget-loading{
|
|
30
|
+
padding:30px;display:flex;align-items:center;justify-content:center;gap:10px;
|
|
31
|
+
color:var(--text-muted);font-size:13px;
|
|
32
|
+
}
|
|
33
|
+
.widget-container .widget-loading .spinner{
|
|
34
|
+
width:18px;height:18px;border:2px solid var(--border-light);
|
|
35
|
+
border-top-color:var(--accent);border-radius:50%;
|
|
36
|
+
animation:spin 0.8s linear infinite;
|
|
37
|
+
}
|
|
38
|
+
@keyframes spin{to{transform:rotate(360deg)}}
|
|
39
|
+
.widget-error{
|
|
40
|
+
padding:16px;color:var(--color-rose);font-size:13px;
|
|
41
|
+
background:var(--accent-soft);border-radius:var(--radius);margin:10px;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.widget-fullscreen-overlay{
|
|
45
|
+
position:fixed;inset:0;z-index:1000;background:var(--bg-primary);
|
|
46
|
+
display:none;flex-direction:column;
|
|
47
|
+
}
|
|
48
|
+
.widget-fullscreen-overlay.active{display:flex}
|
|
49
|
+
.widget-fullscreen-overlay .fs-header{
|
|
50
|
+
height:48px;display:flex;align-items:center;justify-content:space-between;
|
|
51
|
+
padding:0 16px;border-bottom:1px solid var(--border-light);flex-shrink:0;
|
|
52
|
+
background:var(--bg-primary);
|
|
53
|
+
}
|
|
54
|
+
.widget-fullscreen-overlay .fs-header .left{display:flex;align-items:center;gap:8px}
|
|
55
|
+
.widget-fullscreen-overlay .fs-close{
|
|
56
|
+
background:none;border:1px solid var(--border-light);color:var(--text-muted);
|
|
57
|
+
padding:4px 14px;border-radius:8px;font-size:13px;cursor:pointer;
|
|
58
|
+
}
|
|
59
|
+
.widget-fullscreen-overlay .fs-close:hover{border-color:var(--accent);color:var(--accent)}
|
|
60
|
+
.widget-fullscreen-overlay iframe{
|
|
61
|
+
flex:1;width:100%;border:none;
|
|
62
|
+
}
|
package/package.json
CHANGED
|
@@ -1 +1,29 @@
|
|
|
1
|
-
{
|
|
1
|
+
{
|
|
2
|
+
"name": "visualknowledge",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Interactive AI Chat with Visualization - one-click launch via npx",
|
|
5
|
+
"bin": {
|
|
6
|
+
"visualknowledge": "./bin/visualknowledge.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"bin/",
|
|
10
|
+
"frontend/",
|
|
11
|
+
"server.py",
|
|
12
|
+
"skills/"
|
|
13
|
+
],
|
|
14
|
+
"keywords": [
|
|
15
|
+
"ai",
|
|
16
|
+
"chat",
|
|
17
|
+
"visualization",
|
|
18
|
+
"claude",
|
|
19
|
+
"mermaid"
|
|
20
|
+
],
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=16"
|
|
24
|
+
},
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "https://github.com/user/VisualKnowledge"
|
|
28
|
+
}
|
|
29
|
+
}
|
package/server.py
CHANGED
|
@@ -51,14 +51,24 @@ graph TD
|
|
|
51
51
|
- 最后给出总结要点"""
|
|
52
52
|
|
|
53
53
|
|
|
54
|
+
FRONTEND_DIR = os.path.join(BASE_DIR, 'frontend')
|
|
55
|
+
|
|
54
56
|
@app.route('/')
|
|
55
57
|
def index():
|
|
56
|
-
resp = send_from_directory(
|
|
58
|
+
resp = send_from_directory(FRONTEND_DIR, 'index.html')
|
|
57
59
|
resp.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
|
|
58
60
|
resp.headers['Pragma'] = 'no-cache'
|
|
59
61
|
resp.headers['Expires'] = '0'
|
|
60
62
|
return resp
|
|
61
63
|
|
|
64
|
+
@app.route('/<path:path>')
|
|
65
|
+
def static_files(path):
|
|
66
|
+
"""Serve all static files (CSS, JS) from the frontend directory."""
|
|
67
|
+
file_path = os.path.join(FRONTEND_DIR, path)
|
|
68
|
+
if os.path.isfile(file_path):
|
|
69
|
+
return send_from_directory(FRONTEND_DIR, path)
|
|
70
|
+
return ('Not found', 404)
|
|
71
|
+
|
|
62
72
|
|
|
63
73
|
@app.route('/api/models', methods=['GET'])
|
|
64
74
|
def get_models():
|