5 Commits

Author SHA1 Message Date
0b67bbe885 feat: 上传文件功能改进
- 文件上传后显示标记(文件名标签),不填充输入框
- 用户正常输入问题,发送时文件内容自动附加
- 文件内容作为系统消息提交给AI
- 界面显示'已上传文件'标记,可点击移除
- 发送后自动清除文件标记
2026-04-29 22:59:39 +08:00
b98ab79ae2 feat: 上传文件功能优化
- 只支持文本类型文件(txt、md、json、csv、代码文件等)
- 最多读取 10000 字符,超出自动截取
- 文件内容附加到输入框,用户可编辑后发送
- 与用户消息一起提交给大模型处理
- 移除 PDF/DOC 等非文本文件支持
2026-04-29 22:51:06 +08:00
0d88d22509 feat: 根据大模型视觉能力控制上传图片选项显示
- 前端加载 LLM 配置时保存 enable_vision 到 CONFIG
- 上传弹窗根据 CONFIG.enableVision 决定是否显示图片上传选项
- 无视觉能力的大模型只显示上传文件选项
- 普通对话和智能体对话界面都应用此逻辑
2026-04-29 18:31:36 +08:00
7c8adc0d78 feat: 大模型配置增加思考模式和视觉能力选项
- 数据库新增 enable_thinking 和 enable_vision 字段
- 后台管理表格显示思考🧠和视觉👁️能力状态
- 添加/编辑表单增加开关按钮选择
- 兼容旧数据库自动添加新字段
2026-04-29 18:08:27 +08:00
6fd916f57c feat: TTS语音播放优化
- 退出对话界面时自动停止语音播放
- 优化TTS文本清理:添加更多Markdown特殊字符过滤
- 新增:任务列表、图片语法、删除线、表格、脚注引用清理
- 新增:更完整的emoji范围清理(200+个符号)
- 新增:数学符号、货币符号、版权符号清理
2026-04-29 17:38:25 +08:00
5 changed files with 377 additions and 42 deletions

View File

@@ -54,6 +54,8 @@ def init_db():
model TEXT NOT NULL,
max_tokens INTEGER DEFAULT 2048,
temperature REAL DEFAULT 0.7,
enable_thinking INTEGER DEFAULT 0,
enable_vision INTEGER DEFAULT 0,
is_default INTEGER DEFAULT 0,
is_active INTEGER DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
@@ -221,6 +223,14 @@ def init_db():
'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', 10, 1)
''')
# 检测并添加 llm_configs 新字段(兼容旧数据库)
cursor.execute("PRAGMA table_info(llm_configs)")
llm_columns = [col[1] for col in cursor.fetchall()]
if 'enable_thinking' not in llm_columns:
cursor.execute("ALTER TABLE llm_configs ADD COLUMN enable_thinking INTEGER DEFAULT 0")
if 'enable_vision' not in llm_columns:
cursor.execute("ALTER TABLE llm_configs ADD COLUMN enable_vision INTEGER DEFAULT 0")
# 初始化默认对话配置
cursor.execute('SELECT COUNT(*) FROM chat_configs')
if cursor.fetchone()[0] == 0:
@@ -1005,10 +1015,11 @@ def add_llm_config():
conn = get_db()
cursor = conn.cursor()
cursor.execute('''
INSERT INTO llm_configs (name, provider, api_url, api_key, model, max_tokens, temperature)
VALUES (?, ?, ?, ?, ?, ?, ?)
INSERT INTO llm_configs (name, provider, api_url, api_key, model, max_tokens, temperature, enable_thinking, enable_vision)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (data['name'], data['provider'], data['api_url'], data['api_key'],
data['model'], data.get('max_tokens', 2048), data.get('temperature', 0.7)))
data['model'], data.get('max_tokens', 2048), data.get('temperature', 0.7),
data.get('enable_thinking', 0), data.get('enable_vision', 0)))
conn.commit()
config_id = cursor.lastrowid
conn.close()
@@ -1023,9 +1034,10 @@ def update_llm_config(id):
cursor = conn.cursor()
cursor.execute('''
UPDATE llm_configs SET name=?, provider=?, api_url=?, api_key=?, model=?,
max_tokens=?, temperature=?, updated_at=CURRENT_TIMESTAMP WHERE id=?
max_tokens=?, temperature=?, enable_thinking=?, enable_vision=?, updated_at=CURRENT_TIMESTAMP WHERE id=?
''', (data['name'], data['provider'], data['api_url'], data['api_key'],
data['model'], data.get('max_tokens', 2048), data.get('temperature', 0.7), id))
data['model'], data.get('max_tokens', 2048), data.get('temperature', 0.7),
data.get('enable_thinking', 0), data.get('enable_vision', 0), id))
conn.commit()
conn.close()
return jsonify({'success': True})

View File

@@ -401,6 +401,54 @@
.toast.show {
opacity: 1;
}
/* 表单行(双列布局) */
.form-row {
display: flex;
gap: 20px;
margin-bottom: 16px;
}
.form-group-half {
flex: 1;
}
/* 表单提示 */
.form-tip {
font-size: 12px;
color: #999;
margin-top: 4px;
}
/* 开关按钮 */
.toggle-switch {
width: 50px;
height: 26px;
background: #ccc;
border-radius: 13px;
cursor: pointer;
position: relative;
transition: background 0.3s;
}
.toggle-switch.active {
background: var(--primary);
}
.toggle-slider {
width: 22px;
height: 22px;
background: white;
border-radius: 50%;
position: absolute;
top: 2px;
left: 2px;
transition: left 0.3s;
}
.toggle-switch.active .toggle-slider {
left: 26px;
}
</style>
</head>
<body>

View File

@@ -559,7 +559,8 @@ async function loadLLMPage(content) {
<th>名称</th>
<th>提供商</th>
<th>模型</th>
<th>API URL</th>
<th>思考</th>
<th>视觉</th>
<th>状态</th>
<th>操作</th>
</tr>
@@ -570,7 +571,8 @@ async function loadLLMPage(content) {
<td>${c.name} ${c.is_default ? '<span class="default-badge">默认</span>' : ''}</td>
<td>${c.provider}</td>
<td>${c.model}</td>
<td style="max-width: 200px; overflow: hidden; text-overflow: ellipsis;">${c.api_url}</td>
<td>${c.enable_thinking ? '🧠 支持' : '—'}</td>
<td>${c.enable_vision ? '👁️ 支持' : '—'}</td>
<td>${c.is_active ? '✅ 启用' : '❌ 禁用'}</td>
<td>
<div class="action-btns">
@@ -622,8 +624,28 @@ function showAddLLMModal() {
<label class="form-label">Temperature</label>
<input type="number" class="form-input" id="llmTemperature" value="0.7" step="0.1">
</div>
<div class="form-row">
<div class="form-group form-group-half">
<label class="form-label">思考模式 🧠</label>
<div class="toggle-switch" id="llmThinkingToggle" data-value="0">
<div class="toggle-slider"></div>
</div>
<div class="form-tip">支持深度思考/推理能力</div>
</div>
<div class="form-group form-group-half">
<label class="form-label">视觉能力 👁️</label>
<div class="toggle-switch" id="llmVisionToggle" data-value="0">
<div class="toggle-slider"></div>
</div>
<div class="form-tip">支持图片输入/多模态</div>
</div>
</div>
<button class="form-submit" onclick="saveLLM()">保存</button>
`);
// 绑定开关事件
bindToggleSwitch('llmThinkingToggle');
bindToggleSwitch('llmVisionToggle');
}
function showEditLLMModal(id) {
@@ -664,8 +686,28 @@ function showEditLLMModal(id) {
<label class="form-label">Temperature</label>
<input type="number" class="form-input" id="llmTemperature" value="${config.temperature}" step="0.1">
</div>
<div class="form-row">
<div class="form-group form-group-half">
<label class="form-label">思考模式 🧠</label>
<div class="toggle-switch ${config.enable_thinking ? 'active' : ''}" id="llmThinkingToggle" data-value="${config.enable_thinking || 0}">
<div class="toggle-slider"></div>
</div>
<div class="form-tip">支持深度思考/推理能力</div>
</div>
<div class="form-group form-group-half">
<label class="form-label">视觉能力 👁️</label>
<div class="toggle-switch ${config.enable_vision ? 'active' : ''}" id="llmVisionToggle" data-value="${config.enable_vision || 0}">
<div class="toggle-slider"></div>
</div>
<div class="form-tip">支持图片输入/多模态</div>
</div>
</div>
<button class="form-submit" onclick="updateLLM(${id})">保存</button>
`);
// 绑定开关事件
bindToggleSwitch('llmThinkingToggle');
bindToggleSwitch('llmVisionToggle');
}
async function saveLLM() {
@@ -676,7 +718,9 @@ async function saveLLM() {
api_key: document.getElementById('llmApiKey').value,
model: document.getElementById('llmModel').value,
max_tokens: parseInt(document.getElementById('llmMaxTokens').value),
temperature: parseFloat(document.getElementById('llmTemperature').value)
temperature: parseFloat(document.getElementById('llmTemperature').value),
enable_thinking: parseInt(document.getElementById('llmThinkingToggle')?.dataset.value || 0),
enable_vision: parseInt(document.getElementById('llmVisionToggle')?.dataset.value || 0)
};
if (!data.name || !data.api_url || !data.api_key || !data.model) {
@@ -698,7 +742,9 @@ async function updateLLM(id) {
api_key: document.getElementById('llmApiKey').value,
model: document.getElementById('llmModel').value,
max_tokens: parseInt(document.getElementById('llmMaxTokens').value),
temperature: parseFloat(document.getElementById('llmTemperature').value)
temperature: parseFloat(document.getElementById('llmTemperature').value),
enable_thinking: parseInt(document.getElementById('llmThinkingToggle')?.dataset.value || 0),
enable_vision: parseInt(document.getElementById('llmVisionToggle')?.dataset.value || 0)
};
await fetchAPI(`/api/admin/llm/${id}`, 'PUT', data);
@@ -1512,6 +1558,18 @@ function closeModal() {
document.getElementById('modal').classList.remove('show');
}
function bindToggleSwitch(id) {
const toggle = document.getElementById(id);
if (!toggle) return;
toggle.addEventListener('click', () => {
const current = parseInt(toggle.dataset.value) || 0;
const newValue = current === 0 ? 1 : 0;
toggle.dataset.value = newValue;
toggle.classList.toggle('active', newValue === 1);
});
}
function showToast(message) {
const toast = document.getElementById('toast');
toast.textContent = message;

View File

@@ -25,6 +25,9 @@ let systemAgents = [];
// 用户智能体界面显示的智能体
let agents = [];
// 上传的文件(临时存储)
let uploadedFile = null; // { name: string, content: string }
// 用户添加的智能体(按类别分组)
let myAgents = {
basic: [],
@@ -150,6 +153,8 @@ async function loadBackendConfig() {
CONFIG.apiKey = backendConfig.llm.api_key;
CONFIG.model = backendConfig.llm.model;
CONFIG.maxTokens = backendConfig.llm.max_tokens || 2048;
CONFIG.enableThinking = backendConfig.llm.enable_thinking || 0;
CONFIG.enableVision = backendConfig.llm.enable_vision || 0;
}
updateAgentsDisplay();
@@ -518,6 +523,10 @@ function savePinnedAgents() {
// ==================== 主页(底部导航栏) ====================
function showMainPage() {
// 退出对话界面时停止语音播放
stopTTSQueue();
enableTTS = false;
currentConversation = null;
currentAgent = null;
@@ -3230,10 +3239,12 @@ function showAgentChatPage() {
<!-- 上传选项弹窗 -->
<div class="attach-panel" id="attachPanel">
<div class="attach-panel-content">
${CONFIG.enableVision ? `
<div class="attach-item" data-type="image">
<div class="attach-icon">📷</div>
<div class="attach-label">上传图片</div>
</div>
` : ''}
<div class="attach-item" data-type="file">
<div class="attach-icon">📄</div>
<div class="attach-label">上传文件</div>
@@ -3241,7 +3252,7 @@ function showAgentChatPage() {
</div>
</div>
<input type="file" id="imageInput" accept="image/*" style="display:none">
<input type="file" id="fileInput" accept=".txt,.md,.pdf,.doc,.docx,.json,.csv" style="display:none">
<input type="file" id="fileInput" accept=".txt,.md,.json,.csv,.xml,.yaml,.yml,.log,.sql,.html,.css,.js,.py,.java,.c,.cpp,.go,.rs,.sh,.bash,.ini,.conf,.cfg" style="display:none">
<!-- 搜索栏 -->
<div class="search-bar" id="searchBar">
@@ -3759,10 +3770,12 @@ function openConversation(id) {
<!-- 上传选项弹窗 -->
<div class="attach-panel" id="attachPanel">
<div class="attach-panel-content">
${CONFIG.enableVision ? `
<div class="attach-item" data-type="image">
<div class="attach-icon">📷</div>
<div class="attach-label">上传图片</div>
</div>
` : ''}
<div class="attach-item" data-type="file">
<div class="attach-icon">📄</div>
<div class="attach-label">上传文件</div>
@@ -3770,7 +3783,7 @@ function openConversation(id) {
</div>
</div>
<input type="file" id="imageInput" accept="image/*" style="display:none">
<input type="file" id="fileInput" accept=".txt,.md,.pdf,.doc,.docx,.json,.csv" style="display:none">
<input type="file" id="fileInput" accept=".txt,.md,.json,.csv,.xml,.yaml,.yml,.log,.sql,.html,.css,.js,.py,.java,.c,.cpp,.go,.rs,.sh,.bash,.ini,.conf,.cfg" style="display:none">
</div>
`;
@@ -3987,8 +4000,26 @@ async function sendMessage() {
// 隐藏欢迎界面
welcome.style.display = 'none';
// 添加用户消息
currentConversation.messages.push({ role: 'user', content: text });
// 构建用户消息内容
let userContent = text;
let fileContent = null;
// 如果有上传的文件,附加文件内容
if (uploadedFile) {
fileContent = uploadedFile;
userContent = text; // 用户输入的问题
// 发送后清除上传文件标记
uploadedFile = null;
const fileTagArea = document.getElementById('fileTagArea');
if (fileTagArea) fileTagArea.innerHTML = '';
}
// 添加用户消息(界面显示)
const displayContent = fileContent
? `${text}\n\n📎 已上传文件: ${fileContent.name}`
: text;
currentConversation.messages.push({ role: 'user', content: displayContent });
// 更新对话标题(第一条用户消息)
if (currentConversation.title === '新对话') {
@@ -4023,11 +4054,11 @@ async function sendMessage() {
autoResize(userInput);
// 调用流式生成
await streamGenerate(currentConversation.messages.length - 1);
await streamGenerate(currentConversation.messages.length - 1, fileContent);
}
// 流式生成 AI 回复
async function streamGenerate(userMsgIndex) {
async function streamGenerate(userMsgIndex, fileContent = null) {
isLoading = true;
sendBtn.disabled = true;
@@ -4072,6 +4103,14 @@ async function streamGenerate(userMsgIndex) {
content: m.content
}));
// 如果有上传的文件,添加文件内容作为系统消息
if (fileContent) {
messagesToSend.push({
role: 'system',
content: `用户上传了文件 "${fileContent.name}",以下是文件内容:\n\n${fileContent.content}`
});
}
// 如果有搜索结果,将搜索内容添加到消息中
if (searchResults) {
const searchContext = formatSearchResultsForLLM(searchResults);
@@ -4817,9 +4856,18 @@ function cleanTTSText(text) {
// 移除数字列表1.、2.等)
cleaned = cleaned.replace(/^\d+\.\s+/gm, '');
// 移除任务列表([ ]、[x]
cleaned = cleaned.replace(/^\s*\[[x ]\]\s*/gmi, '');
// 处理图片语法 ![alt](url) -> 移除整个图片标记
cleaned = cleaned.replace(/!\[[^\]]*\]\([^)]+\)/g, '');
// 处理链接 [text](url) -> 只保留text
cleaned = cleaned.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1');
// 移除删除线(~~text~~
cleaned = cleaned.replace(/~~([^~]+)~~/g, '$1');
// 移除粗体/斜体符号(**text**、*text*、__text__、_text_
cleaned = cleaned.replace(/\*\*([^*]+)\*\*/g, '$1');
cleaned = cleaned.replace(/\*([^*]+)\*/g, '$1');
@@ -4829,10 +4877,18 @@ function cleanTTSText(text) {
// 移除引用符号(>
cleaned = cleaned.replace(/^>\s*/gm, '');
// 移除分割线(---、***
// 移除分割线(---、***、___
cleaned = cleaned.replace(/^[\-\*]{3,}$/gm, '');
cleaned = cleaned.replace(/^_{3,}$/gm, '');
// 移除表情符号常见emoji范围
// 移除表格分隔符(| 和 ---|--|--
cleaned = cleaned.replace(/^\|.*\|$/gm, ''); // 表格行
cleaned = cleaned.replace(/^\s*[\-\:]+\s*\|[\s\-\:]+\|[\s\-\:]+\s*$/gm, ''); // 表格分隔行
// 移除脚注引用([^1]
cleaned = cleaned.replace(/\[\^[^\]]+\]/g, '');
// 移除表情符号完整emoji范围
cleaned = cleaned.replace(/[\u{1F600}-\u{1F64F}]/gu, ''); // 表情
cleaned = cleaned.replace(/[\u{1F300}-\u{1F5FF}]/gu, ''); // 符号和图形
cleaned = cleaned.replace(/[\u{1F680}-\u{1F6FF}]/gu, ''); // 交通和地图
@@ -4846,11 +4902,74 @@ function cleanTTSText(text) {
cleaned = cleaned.replace(/[\u{2700}-\u{27BF}]/gu, ''); // 装饰符号
cleaned = cleaned.replace(/[\u{FE00}-\u{FE0F}]/gu, ''); // 变体选择符
cleaned = cleaned.replace(/[\u{1F1E0}-\u{1F1FF}]/gu, ''); // 旗帜
cleaned = cleaned.replace(/[\u{1F004}\u{1F0CF}]/gu, ''); // 麻将牌
cleaned = cleaned.replace(/[\u{231A}-\u{231B}]/gu, ''); // 时钟
cleaned = cleaned.replace(/[\u{23E9}-\u{23F3}]/gu, ''); // 其他符号
cleaned = cleaned.replace(/[\u{23F8}-\u{23FA}]/gu, ''); // 暂停/播放等
cleaned = cleaned.replace(/[\u{25AA}-\u{25AB}]/gu, ''); // 小方块
cleaned = cleaned.replace(/[\u{25B6}]/gu, ''); // 播放按钮
cleaned = cleaned.replace(/[\u{25C0}]/gu, ''); // 反向播放
cleaned = cleaned.replace(/[\u{25FB}-\u{25FE}]/gu, ''); // 白色方块
cleaned = cleaned.replace(/[\u{2614}-\u{2615}]/gu, ''); // 雨伞/咖啡
cleaned = cleaned.replace(/[\u{2648}-\u{2653}]/gu, ''); // 星座符号
cleaned = cleaned.replace(/[\u{267F}]/gu, ''); // 轮椅符号
cleaned = cleaned.replace(/[\u{2693}]/gu, ''); // 船锚
cleaned = cleaned.replace(/[\u{26A1}]/gu, ''); // 高压符号
cleaned = cleaned.replace(/[\u{26AA}-\u{26AB}]/gu, ''); // 圆圈
cleaned = cleaned.replace(/[\u{26BD}]/gu, ''); // 足球
cleaned = cleaned.replace(/[\u{26BE}]/gu, ''); // 棒球
cleaned = cleaned.replace(/[\u{26C4}]/gu, ''); // 雪人
cleaned = cleaned.replace(/[\u{26C5}]/gu, ''); // 太阳云
cleaned = cleaned.replace(/[\u{26CE}]/gu, ''); // 星座
cleaned = cleaned.replace(/[\u{26D4}]/gu, ''); // 禁止进入
cleaned = cleaned.replace(/[\u{26EA}]/gu, ''); // 教堂
cleaned = cleaned.replace(/[\u{26F2}]/gu, ''); // 喷泉
cleaned = cleaned.replace(/[\u{26F3}]/gu, ''); // 高尔夫
cleaned = cleaned.replace(/[\u{26F5}]/gu, ''); // 帆船
cleaned = cleaned.replace(/[\u{26FA}]/gu, ''); // 帐篷
cleaned = cleaned.replace(/[\u{26FD}]/gu, ''); // 加油站
cleaned = cleaned.replace(/[\u{2702}]/gu, ''); // 剪刀
cleaned = cleaned.replace(/[\u{2705}]/gu, ''); // 白色对勾
cleaned = cleaned.replace(/[\u{2708}-\u{270D}]/gu, ''); // 飞机/笔等
cleaned = cleaned.replace(/[\u{270F}]/gu, ''); // 铅笔
cleaned = cleaned.replace(/[\u{2712}]/gu, ''); // 笔
cleaned = cleaned.replace(/[\u{2714}]/gu, ''); // 对勾
cleaned = cleaned.replace(/[\u{2716}]/gu, ''); // X标记
cleaned = cleaned.replace(/[\u{271D}]/gu, ''); // 十字架
cleaned = cleaned.replace(/[\u{2721}]/gu, ''); // 六芒星
cleaned = cleaned.replace(/[\u{2728}]/gu, ''); // 星光
cleaned = cleaned.replace(/[\u{2733}-\u{2734}]/gu, ''); // 八角雪花
cleaned = cleaned.replace(/[\u{2744}]/gu, ''); // 雪花
cleaned = cleaned.replace(/[\u{2747}]/gu, ''); // 闪亮
cleaned = cleaned.replace(/[\u{274C}]/gu, ''); // 红色X
cleaned = cleaned.replace(/[\u{274E}]/gu, ''); // 红色方X
cleaned = cleaned.replace(/[\u{2753}-\u{2755}]/gu, ''); // 问号
cleaned = cleaned.replace(/[\u{2757}]/gu, ''); // 感叹号
cleaned = cleaned.replace(/[\u{2763}-\u{2764}]/gu, ''); // 心形
cleaned = cleaned.replace(/[\u{2795}-\u{2797}]/gu, ''); // 加减乘
cleaned = cleaned.replace(/[\u{27A1}]/gu, ''); // 箭头
cleaned = cleaned.replace(/[\u{27B0}]/gu, ''); // 曲线箭头
cleaned = cleaned.replace(/[\u{27BF}]/gu, ''); // 双曲线箭头
cleaned = cleaned.replace(/[\u{2934}-\u{2935}]/gu, ''); // 箭头
cleaned = cleaned.replace(/[\u{2B05}-\u{2B07}]/gu, ''); // 方向箭头
cleaned = cleaned.replace(/[\u{2B1B}-\u{2B1C}]/gu, ''); // 黑白方块
cleaned = cleaned.replace(/[\u{2B50}]/gu, ''); // 中等星
cleaned = cleaned.replace(/[\u{2B55}]/gu, ''); // 圆圈
cleaned = cleaned.replace(/[\u{3030}]/gu, ''); // 波浪线
cleaned = cleaned.replace(/[\u{303D}]/gu, ''); // 花括号
cleaned = cleaned.replace(/[\u{3297}]/gu, ''); // 圆圈日文
cleaned = cleaned.replace(/[\u{3299}]/gu, ''); // 圆圈日文
// 移除HTML实体
cleaned = cleaned.replace(/&[a-zA-Z]+;/g, '');
// 清理多余空白
// 移除特殊符号(数学、货币等)
cleaned = cleaned.replace(/[≤≥≠≈∞∑∏√∫]/g, '');
cleaned = cleaned.replace(/[€£¥₹₽₩]/g, '');
cleaned = cleaned.replace(/[©®™]/g, '');
// 清理多余空白和换行
cleaned = cleaned.replace(/\n+/g, ' ');
cleaned = cleaned.replace(/\s+/g, ' ').trim();
return cleaned;
@@ -5209,39 +5328,93 @@ async function handleFileUpload(e) {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = async (event) => {
const content = event.target.result;
const fileName = file.name;
// 添加用户消息
currentConversation.messages.push({
role: 'user',
content: `[文件: ${fileName}]\\n\\n${content.slice(0, 500)}${content.length > 500 ? '...' : ''}`
});
currentConversation.updatedAt = Date.now();
saveConversations();
renderMessages();
if (welcome) welcome.style.display = 'none';
// 调用AI生成
await streamGenerateWithFile(content, fileName);
};
// 检查文件类型(只支持文本文件)
const allowedExtensions = ['.txt', '.md', '.json', '.csv', '.xml', '.yaml', '.yml', '.log', '.sql', '.html', '.css', '.js', '.py', '.java', '.c', '.cpp', '.go', '.rs', '.sh', '.bash', '.ini', '.conf', '.cfg'];
const fileName = file.name.toLowerCase();
const isAllowed = allowedExtensions.some(ext => fileName.endsWith(ext));
// 根据文件类型读取
if (file.name.endsWith('.pdf') || file.name.endsWith('.doc') || file.name.endsWith('.docx')) {
// PDF/Word文件暂时只显示文件名
showToast('PDF/Word文件暂不支持解析请上传文本文件');
if (!isAllowed) {
showToast('只支持文本类型的文件txt、md、json、代码文件等');
e.target.value = '';
return;
}
// 检查文件大小(限制约 10KB大约 10000 字符)
const maxSizeKB = 15; // 留一点余量
if (file.size > maxSizeKB * 1024) {
showToast(`文件太大,请上传小于 ${maxSizeKB}KB 的文本文件`);
e.target.value = '';
return;
}
showToast('正在读取文件...');
const reader = new FileReader();
reader.onload = async (event) => {
let content = event.target.result;
// 限制最多 10000 字符
if (content.length > 10000) {
content = content.slice(0, 10000);
}
// 存储上传的文件
uploadedFile = {
name: file.name,
content: content
};
// 显示文件标记
showUploadedFileTag();
showToast(`已上传: ${file.name}`);
userInput.focus();
};
reader.onerror = () => {
showToast('文件读取失败');
};
reader.readAsText(file);
e.target.value = '';
}
// 显示上传文件标记
function showUploadedFileTag() {
if (!uploadedFile) return;
// 查找或创建文件标记区域
let fileTagArea = document.getElementById('fileTagArea');
if (!fileTagArea) {
fileTagArea = document.createElement('div');
fileTagArea.id = 'fileTagArea';
fileTagArea.className = 'file-tag-area';
// 插入到输入区域前面
const inputArea = document.querySelector('.input-area');
if (inputArea) {
inputArea.insertBefore(fileTagArea, inputArea.firstChild);
}
}
fileTagArea.innerHTML = `
<div class="file-tag">
<span class="file-tag-icon">📄</span>
<span class="file-tag-name">${uploadedFile.name}</span>
<button class="file-tag-remove" onclick="removeUploadedFile()">✕</button>
</div>
`;
}
// 移除上传的文件
function removeUploadedFile() {
uploadedFile = null;
const fileTagArea = document.getElementById('fileTagArea');
if (fileTagArea) {
fileTagArea.innerHTML = '';
}
showToast('已移除文件');
}
// 带图片的流式生成
async function streamGenerateWithImage(base64, imageName) {
isLoading = true;

View File

@@ -3198,6 +3198,50 @@ body {
color: var(--text-color);
}
/* 文件标记区域 */
.file-tag-area {
padding: 8px 12px;
background: rgba(102, 126, 234, 0.05);
border-radius: 8px;
margin-bottom: 8px;
}
.file-tag {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
background: var(--primary);
color: white;
border-radius: 20px;
font-size: 13px;
}
.file-tag-icon {
font-size: 16px;
}
.file-tag-name {
max-width: 150px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.file-tag-remove {
background: none;
border: none;
color: white;
font-size: 14px;
cursor: pointer;
padding: 0 4px;
opacity: 0.8;
}
.file-tag-remove:hover {
opacity: 1;
}
#userInput {
flex: 1;
padding: 12px 16px;