Skip to content

Commit 82c4a53

Browse files
committed
add chat file tree and document retrival
1 parent 95b3023 commit 82c4a53

File tree

7 files changed

+632
-102
lines changed

7 files changed

+632
-102
lines changed

electron/index.html

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,6 @@ <h1 class="splash-title">LoFS</h1>
260260
<button id="chat-model-button" class="chat-model-button" type="button" aria-haspopup="listbox"
261261
aria-expanded="false">
262262
<span id="chat-model-button-text" class="chat-model-button-text">暂无可用模型</span>
263-
<span class="chat-model-button-icon"></span>
264263
</button>
265264
<div id="chat-model-dropdown" class="chat-model-dropdown" role="listbox" aria-label="聊天模型列表"></div>
266265
</div>
@@ -271,10 +270,9 @@ <h1 class="splash-title">LoFS</h1>
271270
<span id="chat-conversation-updated" class="chat-timestamp"></span>
272271
</div>
273272
<div class="chat-page-header-actions">
274-
<button id="chat-file-toggle-btn" class="chat-header-action-btn" type="button" aria-pressed="false"
273+
<button id="chat-file-toggle-btn" class="chat-header-action-btn chat-header-action-btn--icon-only" type="button" aria-pressed="false"
275274
aria-expanded="false" aria-controls="chat-file-panel">
276275
<span class="chat-header-action-icon" id="chat-file-toggle-icon"></span>
277-
<span class="chat-header-action-text">文件</span>
278276
</button>
279277
</div>
280278
</div>

electron/main.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const { app, BrowserWindow, ipcMain, nativeTheme, nativeImage } = require('electron');
1+
const { app, BrowserWindow, ipcMain, nativeTheme, nativeImage, systemPreferences } = require('electron');
22
const fs = require('fs');
33
const path = require('path');
44

@@ -12,6 +12,19 @@ if (app.isPackaged) {
1212
// 禁用硬件加速以避免GPU相关的Mach端口问题
1313
app.disableHardwareAcceleration();
1414

15+
function normalizeMacTextInputPreferences() {
16+
if (process.platform !== 'darwin') {
17+
return;
18+
}
19+
try {
20+
systemPreferences.setUserDefault('ApplePressAndHoldEnabled', 'boolean', false);
21+
} catch (error) {
22+
console.warn('Failed to normalize ApplePressAndHoldEnabled preference:', error);
23+
}
24+
}
25+
26+
normalizeMacTextInputPreferences();
27+
1528
// 修复 macOS 上的 Mach 端口权限错误
1629
if (process.platform === 'darwin') {
1730
app.commandLine.appendSwitch('--no-sandbox');

electron/src/modules/chat/components/file-panel.js

Lines changed: 47 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ class ChatFilePanel {
99
this.closeIconEl = document.getElementById('chat-file-panel-close-icon');
1010
this.refreshIconEl = document.getElementById('chat-file-refresh-icon');
1111
this.onVisibilityChange = typeof options.onVisibilityChange === 'function' ? options.onVisibilityChange : null;
12-
this.onFullTextToggle = typeof options.onFullTextToggle === 'function' ? options.onFullTextToggle : null;
13-
this.fullTextToggleEl = options.fullTextToggleEl || document.getElementById('chat-fulltext-toggle');
1412
this.runtime = {
1513
fileTreeData: null,
1614
expanded: new Set(),
@@ -31,7 +29,6 @@ class ChatFilePanel {
3129
|| 'http://localhost:8000/api/document/upload-status';
3230
this.loadInFlight = null;
3331
this.pendingSelectionBroadcast = null;
34-
this.fullTextEnabled = Boolean(this.fullTextToggleEl?.checked);
3532
this.initialize();
3633
}
3734

@@ -48,7 +45,6 @@ class ChatFilePanel {
4845

4946
this.bindPanelToggle();
5047
this.bindKeyboardShortcuts();
51-
this.bindFullTextToggle();
5248

5349
if (this.container) {
5450
this.container.addEventListener('contextmenu', (event) => {
@@ -62,25 +58,6 @@ class ChatFilePanel {
6258
}
6359
}
6460

65-
bindFullTextToggle() {
66-
if (!this.fullTextToggleEl) {
67-
return;
68-
}
69-
this.fullTextToggleEl.checked = this.fullTextEnabled;
70-
this.fullTextToggleEl.setAttribute('aria-checked', this.fullTextEnabled ? 'true' : 'false');
71-
this.fullTextToggleEl.addEventListener('change', () => {
72-
this.fullTextEnabled = Boolean(this.fullTextToggleEl.checked);
73-
this.fullTextToggleEl.setAttribute('aria-checked', this.fullTextEnabled ? 'true' : 'false');
74-
if (typeof this.onFullTextToggle === 'function') {
75-
this.onFullTextToggle(this.fullTextEnabled);
76-
}
77-
const evt = new CustomEvent('chatFullTextModeChanged', {
78-
detail: { enabled: this.fullTextEnabled }
79-
});
80-
document.dispatchEvent(evt);
81-
});
82-
}
83-
8461
bindPanelToggle() {
8562
if (this.toggleBtn) {
8663
this.toggleBtn.addEventListener('click', () => {
@@ -266,11 +243,11 @@ class ChatFilePanel {
266243
if (isFolder) {
267244
item.classList.add('folder-item');
268245
} else {
269-
item.classList.add('file-item-file', 'is-selectable');
246+
item.classList.add('file-item-file');
270247
}
271248
item.dataset.path = node.path;
272249
item.dataset.relativePath = relativePath;
273-
const fileType = isFolder ? 'folder' : (this.isImageNode(node) ? 'image' : 'file');
250+
const fileType = this.resolveNodeFileType(node, isFolder);
274251
item.dataset.type = fileType;
275252
item.dataset.fileType = fileType;
276253
this.fileTypeLookup.set(node.path, fileType);
@@ -287,11 +264,22 @@ class ChatFilePanel {
287264
}
288265
}
289266
item.setAttribute('aria-selected', this.runtime.selected.has(node.path) ? 'true' : 'false');
290-
if (this.runtime.selected.has(node.path)) {
267+
if (this.runtime.selected.has(node.path) && fileType !== 'unsupported') {
291268
item.dataset.selected = 'true';
292269
}
270+
if (!isFolder) {
271+
item.dataset.fileName = node.name;
272+
if (fileType === 'unsupported') {
273+
item.classList.add('is-disabled');
274+
item.setAttribute('aria-disabled', 'true');
275+
} else {
276+
item.classList.add('is-selectable');
277+
}
278+
}
279+
if (fileType === 'unsupported') {
280+
this.runtime.selected.delete(node.path);
281+
}
293282
this.nodeLookup.set(node.path, node);
294-
295283
const content = document.createElement('div');
296284
content.className = 'file-item-content';
297285
item.appendChild(content);
@@ -300,10 +288,6 @@ class ChatFilePanel {
300288
nameWrapper.className = 'file-name';
301289
content.appendChild(nameWrapper);
302290

303-
if (!isFolder) {
304-
item.dataset.fileName = node.name;
305-
}
306-
307291
const bullet = document.createElement('span');
308292
bullet.className = 'file-bullet';
309293
bullet.textContent = '•';
@@ -325,6 +309,10 @@ class ChatFilePanel {
325309
this.toggleFolder(node.path);
326310
return;
327311
}
312+
if (fileType === 'unsupported') {
313+
this.showSelectionWarning('暂不支持选择 Excel 文件,请选择其它类型的文档。');
314+
return;
315+
}
328316
this.toggleSelection(node.path);
329317
});
330318

@@ -479,6 +467,9 @@ class ChatFilePanel {
479467
return;
480468
}
481469
const type = this.getFileTypeByPath(path);
470+
if (type === 'unsupported') {
471+
return;
472+
}
482473
const relativePath = this.getRelativePathByPath(path);
483474
entries.push({
484475
path,
@@ -550,10 +541,12 @@ class ChatFilePanel {
550541
}
551542
this.container.querySelectorAll('.file-item-file').forEach((item) => {
552543
const type = item.dataset.fileType;
553-
if (imageOnly && type !== 'image') {
544+
const baseDisabled = type === 'unsupported';
545+
const disabledForImageSelection = imageOnly && type !== 'image';
546+
if (baseDisabled || disabledForImageSelection) {
554547
item.classList.add('is-disabled');
555548
item.setAttribute('aria-disabled', 'true');
556-
} else {
549+
} else if (!baseDisabled) {
557550
item.classList.remove('is-disabled');
558551
item.removeAttribute('aria-disabled');
559552
}
@@ -568,6 +561,27 @@ class ChatFilePanel {
568561
return /\.(png|jpe?g|gif|bmp|webp|svg)$/i.test(name);
569562
}
570563

564+
isUnsupportedNode(node) {
565+
if (!node || node.children) {
566+
return false;
567+
}
568+
const name = node.name || '';
569+
return /\.(xlsx|xls)$/i.test(name);
570+
}
571+
572+
resolveNodeFileType(node, isFolder) {
573+
if (isFolder) {
574+
return 'folder';
575+
}
576+
if (this.isImageNode(node)) {
577+
return 'image';
578+
}
579+
if (this.isUnsupportedNode(node)) {
580+
return 'unsupported';
581+
}
582+
return 'file';
583+
}
584+
571585
showSelectionWarning(message) {
572586
if (typeof window.showAlert === 'function') {
573587
window.showAlert(message, 'warning');

electron/src/modules/chat/components/reference-manager.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -700,6 +700,7 @@
700700
});
701701
}
702702

703+
703704
isSameReferenceChunk(left, right) {
704705
if (!left || !right) {
705706
return false;

electron/src/modules/chat/main.js

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,6 @@ class ChatModule {
7777
this.chatFileTreeEl = document.getElementById('chat-file-tree');
7878
this.chatFilePanelCloseBtn = document.getElementById('chat-file-panel-close');
7979
this.chatFileRefreshBtn = document.getElementById('chat-file-refresh-btn');
80-
this.chatFulltextToggleEl = document.getElementById('chat-fulltext-toggle');
8180
this.sendBtnDefaultContent = this.chatSendBtn ? this.chatSendBtn.innerHTML : '';
8281
this.sendBtnDefaultAriaLabel = this.chatSendBtn
8382
? (this.chatSendBtn.getAttribute('aria-label') || '发送消息')
@@ -125,7 +124,6 @@ class ChatModule {
125124
this.unsubscribeChatFileSelection = null;
126125
this.boundHandleResize = null;
127126
this.filePanelForcesCollapse = false;
128-
this.fulltextEnabled = false;
129127
}
130128

131129
initializeManagers() {
@@ -143,15 +141,12 @@ class ChatModule {
143141
toggleBtn: this.chatFileToggleBtn,
144142
closeBtn: this.chatFilePanelCloseBtn,
145143
refreshBtn: this.chatFileRefreshBtn,
146-
fullTextToggleEl: this.chatFulltextToggleEl,
147-
onVisibilityChange: (visible) => this.handleChatFilePanelVisibility(visible),
148-
onFullTextToggle: (enabled) => this.handleFullTextToggle(enabled)
144+
onVisibilityChange: (visible) => this.handleChatFilePanelVisibility(visible)
149145
});
150146
window.chatFilePanel = this.chatFilePanel;
151147
this.unsubscribeChatFileSelection = this.chatFilePanel.onSelectionChange(
152148
(paths, entries) => this.handleChatFileSelection(paths, entries)
153149
);
154-
this.handleFullTextToggle(Boolean(this.chatFulltextToggleEl?.checked));
155150
} catch (error) {
156151
console.error('初始化聊天文件面板失败:', error);
157152
}
@@ -191,14 +186,6 @@ class ChatModule {
191186
}));
192187
}
193188

194-
handleFullTextToggle(enabled) {
195-
this.fulltextEnabled = Boolean(enabled);
196-
}
197-
198-
isFullTextEnabled() {
199-
return Boolean(this.fulltextEnabled);
200-
}
201-
202189
handleChatFilePanelVisibility(visible) {
203190
if (visible) {
204191
this.filePanelForcesCollapse = true;
@@ -3129,12 +3116,7 @@ class ChatModule {
31293116
}
31303117

31313118
const selectedFilePayload = this.getSelectedChatFiles();
3132-
const fullTextToggleEnabled = this.isFullTextEnabled();
3133-
let shouldUseFullText = fullTextToggleEnabled && selectedFilePayload.length > 0;
3134-
if (shouldUseFullText) {
3135-
const hasNonImageTargets = selectedFilePayload.some((entry) => entry && !entry.is_image);
3136-
shouldUseFullText = hasNonImageTargets;
3137-
}
3119+
const shouldUseFullText = selectedFilePayload.length > 0;
31383120

31393121
const metadataPayload = {};
31403122
if (messageAttachments.length > 0) {

electron/src/styles/chat.css

Lines changed: 44 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,18 @@
389389
line-height: 1;
390390
}
391391

392+
.chat-header-action-btn--icon-only {
393+
padding: 6px;
394+
border: none;
395+
background: transparent;
396+
border-radius: 999px;
397+
color: var(--text-muted);
398+
width: 32px;
399+
height: 32px;
400+
justify-content: center;
401+
gap: 0;
402+
}
403+
392404
.chat-header-action-btn:hover,
393405
.chat-header-action-btn:focus-visible {
394406
border-color: var(--accent-color);
@@ -397,18 +409,34 @@
397409
outline: none;
398410
}
399411

412+
.chat-header-action-btn--icon-only:hover,
413+
.chat-header-action-btn--icon-only:focus-visible {
414+
background: rgba(59, 130, 246, 0.12);
415+
border: none;
416+
}
417+
400418
.chat-header-action-btn[aria-pressed="true"] {
401419
border-color: var(--accent-color);
402420
background: rgba(59, 130, 246, 0.14);
403421
color: var(--accent-color);
404422
}
405423

424+
.chat-header-action-btn--icon-only[aria-pressed="true"] {
425+
background: rgba(59, 130, 246, 0.18);
426+
border: none;
427+
}
428+
406429
.chat-header-action-btn.has-selection {
407430
border-color: var(--accent-color);
408431
background: rgba(59, 130, 246, 0.12);
409432
color: var(--accent-color);
410433
}
411434

435+
.chat-header-action-btn--icon-only.has-selection {
436+
border: none;
437+
background: rgba(59, 130, 246, 0.18);
438+
}
439+
412440
.dark-mode .chat-header-action-btn {
413441
background: rgba(71, 85, 105, 0.4);
414442
color: rgba(226, 232, 240, 0.92);
@@ -422,6 +450,20 @@
422450
border-color: rgba(191, 219, 254, 0.5);
423451
}
424452

453+
.dark-mode .chat-header-action-btn--icon-only {
454+
background: transparent;
455+
color: rgba(203, 213, 225, 0.9);
456+
}
457+
458+
.dark-mode .chat-header-action-btn--icon-only:hover,
459+
.dark-mode .chat-header-action-btn--icon-only:focus-visible,
460+
.dark-mode .chat-header-action-btn--icon-only[aria-pressed="true"],
461+
.dark-mode .chat-header-action-btn--icon-only.has-selection {
462+
background: rgba(96, 165, 250, 0.16);
463+
border: none;
464+
color: rgba(191, 219, 254, 0.98);
465+
}
466+
425467
.dark-mode .chat-header-action-btn.has-selection {
426468
background: rgba(96, 165, 250, 0.2);
427469
color: rgba(191, 219, 254, 0.95);
@@ -474,21 +516,6 @@
474516
text-overflow: ellipsis;
475517
}
476518

477-
.chat-model-button-icon {
478-
width: 7px;
479-
height: 7px;
480-
border-right: 1.4px solid currentColor;
481-
border-bottom: 1.4px solid currentColor;
482-
transform: rotate(45deg);
483-
opacity: 0.6;
484-
transition: transform 0.2s ease, opacity 0.2s ease;
485-
}
486-
487-
.chat-model-button[aria-expanded="true"] .chat-model-button-icon {
488-
transform: rotate(-135deg);
489-
opacity: 0.85;
490-
}
491-
492519
.chat-model-dropdown {
493520
position: absolute;
494521
top: calc(100% + 8px);
@@ -691,7 +718,7 @@
691718
position: relative;
692719
flex: 0 0 auto;
693720
width: 0;
694-
max-width: 360px;
721+
max-width: 320px;
695722
background: var(--tree-bg);
696723
border-left: none;
697724
display: flex;
@@ -704,7 +731,7 @@
704731
}
705732

706733
.chat-file-panel.is-open {
707-
width: clamp(260px, 28vw, 320px);
734+
width: clamp(220px, 24vw, 280px);
708735
border-left: 1px solid var(--tree-border);
709736
box-shadow: -6px 0 24px rgba(15, 23, 42, 0.12);
710737
pointer-events: auto;

0 commit comments

Comments
 (0)