⋮
115 :format-search-time="formatSearchTime"
116 + :conversation-loading="conversationsLoading"
117 @toggle-scroll-lock="toggleScrollLock"
⋮
171
171 -
172 -
173 -
174 -
正在加载
175 -
176 - 对话
177 - 工具
178 - 思考
179 - 文件
180 - 对话
181 -
182 -
183 -
184 -
172
• Edited static/src/components/chat/ChatArea.vue (+16 -0)
2
3 +
4 +
5 +
正在加载
6 +
7 + 对话
8 + 工具
9 + 思考
10 + 文件
11 + 对话
12 +
13 +
14 +
15 +
16
⋮
138
139 +
140
⋮
167 formatSearchTime
168 + conversationLoading
169 } = defineProps<{
⋮
176 formatSearchTime: (filters: Record
) => string;
177 + conversationLoading: boolean;
178 }>();
• Edited static/src/styles/layout/_app-shell.scss (+0 -95)
122
123 -.conversation-loading {
124 - min-height: var(--app-viewport, 100vh);
125 - display: flex;
126 - align-items: center;
127 - justify-content: center;
128 - background: var(--claude-bg);
129 - padding: 24px;
130 -}
131 -
132 -.loading-card {
133 - background: rgba(0, 0, 0, 0.65);
134 - border-radius: 20px;
135 - padding: 18px 28px;
136 - color: #fff;
137 - box-shadow: 0 20px 40px rgba(0, 0, 0, 0.18);
138 -}
139 -
140 -.loader {
141 - font-family: 'Iowan Old Style', ui-serif, Georgia, serif;
142 - font-size: 22px;
143 - font-weight: 500;
144 - display: flex;
145 - align-items: center;
146 - gap: 8px;
147 - letter-spacing: 0.04em;
148 - color: rgba(255, 255, 255, 0.9);
149 -}
150 -
151 -.loader p {
152 - margin: 0;
153 -}
154 -
155 -.words {
156 - overflow: hidden;
157 - position: relative;
158 - height: 28px;
159 - line-height: 28px;
160 -}
161 -
162 -.words::after {
163 - content: '';
164 - position: absolute;
165 - inset: 0;
166 - background: linear-gradient(
167 - rgba(0, 0, 0, 0.65) 10%,
168 - transparent 30%,
169 - transparent 70%,
170 - rgba(0, 0, 0, 0.65) 90%
171 - );
172 - z-index: 1;
173 -}
174 -
175 -.word {
176 - display: block;
177 - height: 28px;
178 - color: var(--claude-accent);
179 - animation: conversation-spin 4.5s linear infinite;
180 -}
181 -
182 -.word:nth-child(2) {
183 - animation-delay: -0.9s;
184 -}
185 -
186 -.word:nth-child(3) {
187 - animation-delay: -1.8s;
188 -}
189 -
190 -.word:nth-child(4) {
191 - animation-delay: -2.7s;
192 -}
193 -
194 -.word:nth-child(5) {
195 - animation-delay: -3.6s;
196 -}
197 -
198 -@keyframes conversation-spin {
199 - 0% {
200 - transform: translateY(0%);
201 - }
202 - 20% {
203 - transform: translateY(-100%);
204 - }
205 - 40% {
206 - transform: translateY(-200%);
207 - }
208 - 60% {
209 - transform: translateY(-300%);
210 - }
211 - 80% {
212 - transform: translateY(-400%);
213 - }
214 - 100% {
215 - transform: translateY(-400%);
216 - }
217 -}
123
• Explored
└ Read _chat-area.scss
• Edited static/src/styles/components/chat/_chat-area.scss (+66 -0)
32
33 +.conversation-loader {
34 + min-height: calc(var(--app-viewport, 100vh) - 200px);
35 + display: flex;
36 + align-items: center;
37 + justify-content: center;
38 + text-align: center;
39 + color: var(--claude-text);
40 + font-family: 'Iowan Old Style', ui-serif, Georgia, serif;
41 + letter-spacing: 0.03em;
42 +}
43 +
44 +.conversation-loader .loader {
45 + display: flex;
46 + align-items: center;
47 + gap: 10px;
48 + font-size: 20px;
49 +}
50 +
51 +.conversation-loader .loader p {
52 + margin: 0;
53 + color: var(--claude-text);
54 +}
55 +
56 +.conversation-loader .words {
57 + overflow: hidden;
58 + position: relative;
59 + height: 28px;
60 + line-height: 28px;
61 +}
62 +
63 +.conversation-loader .words::after {
64 + content: '';
65 + position: absolute;
66 + inset: 0;
67 + background: linear-gradient(
68 + rgba(255, 255, 255, 0.9) 5%,
69 + transparent 30%,
70 + transparent 70%,
71 + rgba(255, 255, 255, 0.9) 95%
72 + );
73 + pointer-events: none;
74 +}
75 +
76 +.conversation-loader .word {
77 + display: block;
78 + height: 28px;
79 + color: var(--claude-accent);
80 + animation: conversation-word-spin 4s linear infinite;
81 +}
82 +
83 +.conversation-loader .word:nth-child(2) {
84 + animation-delay: -0.8s;
85 +}
86 +
87 +.conversation-loader .word:nth-child(3) {
88 + animation-delay: -1.6s;
89 +}
90 +
91 +.conversation-loader .word:nth-child(4) {
92 + animation-delay: -2.4s;
93 +}
94 +
95 +.conversation-loader .word:nth-child(5) {
96 + animation-delay: -3.2s;
97 +}
98 +
99 .scroll-lock-toggle {
• Edited static/src/styles/components/chat/_chat-area.scss (+22 -0)
511
512 +
513 +.conversation-loader .word:nth-child(5) {
514 + animation-delay: -3.2s;
515 +}
516 +
517 +@keyframes conversation-word-spin {
518 + 0% {
519 + transform: translateY(0%);
520 + }
521 + 25% {
522 + transform: translateY(-100%);
523 + }
524 + 50% {
525 + transform: translateY(-200%);
526 + }
527 + 75% {
528 + transform: translateY(-300%);
529 + }
530 + 100% {
531 + transform: translateY(-400%);
532 + }
533 +}
534 /* 代码块内容区 */
• Edited static/src/components/chat/ChatArea.vue (+1 -1)
166 formatSearchTopic,
167 - formatSearchTime
167 + formatSearchTime,
168 conversationLoading
Edited static/src/components/chat/ChatArea.vue (+1 -13)
2
3 -
4 -
5 -
正在加载
6 -
7 - 对话
8 - 工具
9 - 思考
10 - 文件
11 - 对话
12 -
13 -
14 -
15 -
3 +
4
• Edited static/src/components/chat/ChatArea.vue (+0 -0)
• Edited static/src/components/chat/ChatArea.vue (+1 -3)
154 formatSearchTopic,
155 - formatSearchTime,
156 - conversationLoading
155 + formatSearchTime
156 } = defineProps<{
⋮
163 formatSearchTime: (filters: Record
) => string;
165 - conversationLoading: boolean;
164 }>();
• Edited static/src/components/chat/ChatArea.vue (+0 -0)
• Edited static/src/styles/components/chat/_chat-area.scss (+0 -65)
32
33 -.conversation-loader {
34 - min-height: calc(var(--app-viewport, 100vh) - 200px);
35 - display: flex;
36 - align-items: center;
37 - justify-content: center;
38 - text-align: center;
39 - color: var(--claude-text);
40 - font-family: 'Iowan Old Style', ui-serif, Georgia, serif;
41 - letter-spacing: 0.03em;
42 -}
43 -
44 -.conversation-loader .loader {
45 - display: flex;
46 - align-items: center;
47 - gap: 10px;
48 - font-size: 20px;
49 -}
50 -
51 -.conversation-loader .loader p {
52 - margin: 0;
53 - color: var(--claude-text);
54 -}
55 -
56 -.conversation-loader .words {
57 - overflow: hidden;
58 - position: relative;
59 - height: 28px;
60 - line-height: 28px;
61 -}
62 -
63 -.conversation-loader .words::after {
64 - content: '';
65 - position: absolute;
66 - inset: 0;
67 - background: linear-gradient(
68 - rgba(255, 255, 255, 0.9) 5%,
69 - transparent 30%,
70 - transparent 70%,
71 - rgba(255, 255, 255, 0.9) 95%
72 - );
73 - pointer-events: none;
74 -}
75 -
76 -.conversation-loader .word {
77 - display: block;
78 - height: 28px;
79 - color: var(--claude-accent);
80 - animation: conversation-word-spin 4s linear infinite;
81 -}
82 -
83 -.conversation-loader .word:nth-child(2) {
84 - animation-delay: -0.8s;
85 -}
86 -
87 -.conversation-loader .word:nth-child(3) {
88 - animation-delay: -1.6s;
89 -}
90 -
91 -.conversation-loader .word:nth-child(4) {
92 - animation-delay: -2.4s;
93 -}
94 -
95 -.conversation-loader .word:nth-child(5) {
96 - animation-delay: -3.2s;
97 -}
33
• Edited static/src/styles/components/chat/_chat-area.scss (+0 -23)
445 }
446 -
447 -
448 -.conversation-loader .word:nth-child(5) {
449 - animation-delay: -3.2s;
450 -}
451 -
452 -@keyframes conversation-word-spin {
453 - 0% {
454 - transform: translateY(0%);
455 - }
456 - 25% {
457 - transform: translateY(-100%);
458 - }
459 - 50% {
460 - transform: translateY(-200%);
461 - }
462 - 75% {
463 - transform: translateY(-300%);
464 - }
465 - 100% {
466 - transform: translateY(-400%);
467 - }
468 -}
446 /* 代码块内容区 */
Edited static/src/App.vue (+5 -2)
36
37 -
37 +
38
⋮
106
107 -
171 +
172 + 正在加载对话…
173 +
174
• Edited static/src/styles/layout/_app-shell.scss (+11 -0)
122
123 +.conversation-loading-simple {
124 + min-height: var(--app-viewport, 100vh);
125 + display: flex;
126 + align-items: center;
127 + justify-content: center;
128 + font-size: 18px;
129 + color: var(--claude-text);
130 + letter-spacing: 0.05em;
131 + background: var(--claude-bg);
132 +}
133 +
134
Edited static/src/App.vue (+8 -0)
1 +
2
⋮
1085
1086 +
1087 +
1088 +
Edited static/src/app.ts (+0 -1)
12 import InputComposer from './components/input/InputComposer.vue';
13 -import ChatArea from './components/chat/ChatArea.vue';
13 import AppShell from './components/shell/AppShell.vue';
• Edited static/src/app.ts (+0 -1)
1930 InputComposer,
1931 - ChatArea,
1931 AppShell
• Edited static/src/app.ts (+5 -1)
196 'minPanelWidth',
197 - 'maxPanelWidth'
197 + 'maxPanelWidth',
198 + 'quotaToast',
199 + 'toastQueue',
200 + 'confirmDialog',
201 + 'easterEgg'
202 ]),
Edited static/src/app.ts (+56 -0)
458
459 + hasContainerStats() {
460 + return !!(
461 + this.containerStatus &&
462 + this.containerStatus.mode === 'docker' &&
463 + this.containerStatus.stats
464 + );
465 + },
466 +
467 + containerStatusClass() {
468 + if (!this.containerStatus) {
469 + return 'status-pill--host';
470 + }
471 + if (this.containerStatus.mode !== 'docker') {
472 + return 'status-pill--host';
473 + }
474 + const rawStatus = (
475 + this.containerStatus.state &&
476 + (this.containerStatus.state.status || this.containerStatus.state.Status)
477 + ) || '';
478 + const status = String(rawStatus).toLowerCase();
479 + if (status.includes('running')) {
480 + return 'status-pill--running';
481 + }
482 + if (status.includes('paused')) {
483 + return 'status-pill--stopped';
484 + }
485 + if (status.includes('exited') || status.includes('dead')) {
486 + return 'status-pill--stopped';
487 + }
488 + return 'status-pill--running';
489 + },
490 +
491 + containerStatusText() {
492 + if (!this.containerStatus) {
493 + return '未知';
494 + }
495 + if (this.containerStatus.mode !== 'docker') {
496 + return '宿主机模式';
497 + }
498 + const rawStatus = (
499 + this.containerStatus.state &&
500 + (this.containerStatus.state.status || this.containerStatus.state.Status)
501 + ) || '';
502 + const status = String(rawStatus).toLowerCase();
503 + if (status.includes('running')) {
504 + return '运行中';
505 + }
506 + if (status.includes('paused')) {
507 + return '已暂停';
508 + }
509 + if (status.includes('exited') || status.includes('dead')) {
510 + return '已停止';
511 + }
512 + return rawStatus || '容器模式';
513 + },
514 +
515 async bootstrapRoute() {
• Edited static/src/app.ts (+5 -0)
29 import { useEasterEgg } from './composables/useEasterEgg';
30 +import { renderMarkdown as renderMarkdownHelper } from './composables/useMarkdownRenderer';
31 import {
⋮
459
460 + renderMarkdown(content, isStreaming = false) {
461 + return renderMarkdownHelper(content, isStreaming);
462 + },
463 +
464 hasContainerStats() {
• Edited static/src/app.ts (+1 -0)
250 ]),
251 + ...mapWritableState(useFocusStore, ['focusedFiles']),
252 ...mapWritableState(useUploadStore, ['uploading'])
Edited static/src/app.ts (+45 -0)
520
521 + formatTime(value) {
522 + if (!value) {
523 + return '未知时间';
524 + }
525 + let date;
526 + if (typeof value === 'number') {
527 + date = new Date(value);
528 + } else if (typeof value === 'string') {
529 + const parsed = Date.parse(value);
530 + if (!Number.isNaN(parsed)) {
531 + date = new Date(parsed);
532 + } else {
533 + const numeric = Number(value);
534 + if (!Number.isNaN(numeric)) {
535 + date = new Date(numeric);
536 + }
537 + }
538 + } else if (value instanceof Date) {
539 + date = value;
540 + }
541 + if (!date || Number.isNaN(date.getTime())) {
542 + return String(value);
543 + }
544 + const now = Date.now();
545 + const diff = now - date.getTime();
546 + if (diff < 60000) {
547 + return '刚刚';
548 + }
549 + if (diff < 3600000) {
550 + const mins = Math.floor(diff / 60000);
551 + return `${mins} 分钟前`;
552 + }
553 + if (diff < 86400000) {
554 + const hours = Math.floor(diff / 3600000);
555 + return `${hours} 小时前`;
556 + }
557 + const formatter = new Intl.DateTimeFormat('zh-CN', {
558 + month: '2-digit',
559 + day: '2-digit',
560 + hour: '2-digit',
561 + minute: '2-digit'
562 + });
563 + return formatter.format(date);
564 + },
565 +
566 async bootstrapRoute() {
Edited static/src/app.ts (+19 -0)
122 skipConversationLoadedEvent: false,
123 + skipConversationHistoryReload: false,
124
⋮
272 this.autoResizeInput();
273 + },
274 + currentConversationId: {
275 + immediate: false,
276 + handler(newValue, oldValue) {
277 + if (!newValue || typeof newValue !== 'string' || newValue.startsWith('temp_')) {
278 + return;
279 + }
280 + if (this.skipConversationHistoryReload) {
281 + this.skipConversationHistoryReload = false;
282 + return;
283 + }
284 + if (oldValue && newValue === oldValue) {
285 + return;
286 + }
287 + this.fetchAndDisplayHistory();
288 + this.fetchConversationTokenStatistics();
289 + this.updateCurrentContextTokens();
290 + }
291 }
• Explored
└ Search fetchAndDisplayHistory in app.ts
Read app.ts
• Edited static/src/app.ts (+2 -0)
834 if (!this.currentConversationId) {
835 + this.skipConversationHistoryReload = true;
836 this.currentConversationId = statusConversationId;
⋮
987 // 2. 更新当前对话信息
988 + this.skipConversationHistoryReload = true;
989 this.currentConversationId = conversationId;
Edited static/src/composables/useLegacySocket.ts (+24 -1)
22
23 + const scheduleHistoryReload = (delay = 0) => {
24 + if (!ctx || typeof ctx.fetchAndDisplayHistory !== 'function') {
25 + return;
26 + }
27 + if (!ctx.currentConversationId || ctx.currentConversationId.startsWith('temp_')) {
28 + return;
29 + }
30 + setTimeout(() => {
31 + try {
32 + ctx.fetchAndDisplayHistory();
33 + if (typeof ctx.fetchConversationTokenStatistics === 'function') {
34 + ctx.fetchConversationTokenStatistics();
35 + }
36 + if (typeof ctx.updateCurrentContextTokens === 'function') {
37 + ctx.updateCurrentContextTokens();
38 + }
39 + } catch (error) {
40 + console.warn('重新加载对话历史失败:', error);
41 + }
42 + }, delay);
43 + };
44 +
45 const assignSocketToken = async () => {
⋮
69 console.log('WebSocket已连接');
48 - // 连接时重置所有状态
70 + // 连接时重置所有状态并刷新当前对话
71 ctx.resetAllStates();
72 + scheduleHistoryReload(200);
73 });
Edited static/src/app.ts (+10 -4)
156 await this.initSocket();
157 - this.initScrollListener();
157 + this.$nextTick(() => {
158 + this.initScrollListener();
159 + });
160
⋮
175 });
174 - this.resourceStartContainerStatsPolling();
175 - this.resourceStartProjectStoragePolling();
176 - this.resourceStartUsageQuotaPolling();
176 + this.resourceStartContainerStatsPolling();
177 + this.resourceStartProjectStoragePolling();
178 + this.resourceStartUsageQuotaPolling();
179 },
⋮
802 console.log('前端状态重置完成');
803 +
804 + this.$nextTick(() => {
805 + this.initScrollListener();
806 + });
807 },
Edited static/src/app.ts (+11 -0)
296 methods: {
297 + ensureScrollListener() {
298 + if (this._scrollListenerReady) {
299 + return;
300 + }
301 + const area = this.getMessagesAreaElement();
302 + if (!area) {
303 + return;
304 + }
305 + this.initScrollListener();
306 + this._scrollListenerReady = true;
307 + },
308 ...mapActions(useUiStore, {
• Explored
└ Read app.ts
• Edited static/src/app.ts (+3 -2)
716 }
717 + this._scrollListenerReady = true;
718
⋮
814 console.log('前端状态重置完成');
814 -
815 + this._scrollListenerReady = false;
816 this.$nextTick(() => {
816 - this.initScrollListener();
817 + this.ensureScrollListener();
818 });
• Edited static/src/app.ts (+1 -1)
157 this.$nextTick(() => {
158 - this.initScrollListener();
158 + this.ensureScrollListener();
159 });
• Explored
└ Search _scrollListenerReady in app.ts
Search _scrollListenerReady
• Edited static/src/app.ts (+1 -0)
123 skipConversationHistoryReload: false,
124 + _scrollListenerReady: false,
125
• Edited static/src/app.ts (+11 -0)
819 });
820 +
821 + const activeConversationId = this.currentConversationId;
822 + if (activeConversationId && !activeConversationId.startsWith('temp_')) {
823 + setTimeout(() => {
824 + if (this.currentConversationId === activeConversationId) {
825 + this.fetchAndDisplayHistory();
826 + this.fetchConversationTokenStatistics();
827 + this.updateCurrentContextTokens();
828 + }
829 + }, 250);
830 + }
831 },