mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-05-19 21:00:25 +08:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 120eb93c23 | |||
| 0f4a0da536 | |||
| 071a8dbb2f | |||
| 68e7285570 | |||
| a6e574115a | |||
| ad62f9e727 | |||
| c3e165409a | |||
| 2be18adf18 |
@@ -312,7 +312,7 @@ jobs:
|
||||
echo ""
|
||||
echo "---"
|
||||
echo ""
|
||||
echo "Full Changelog: [English](https://github.com/hellodigua/ChatLab/blob/main/docs/public/changelogs/en.json) | [中文](https://github.com/hellodigua/ChatLab/blob/main/docs/public/changelogs/cn.json) | [日本語](https://github.com/hellodigua/ChatLab/blob/main/docs/public/changelogs/ja.json)"
|
||||
echo "Full Changelog: [English](https://github.com/ChatLab/ChatLab/blob/main/docs/public/changelogs/en.json) | [中文](https://github.com/ChatLab/ChatLab/blob/main/docs/public/changelogs/cn.json) | [日本語](https://github.com/ChatLab/ChatLab/blob/main/docs/public/changelogs/ja.json)"
|
||||
echo ""
|
||||
echo "---"
|
||||
echo ""
|
||||
@@ -320,12 +320,12 @@ jobs:
|
||||
echo ""
|
||||
echo "| Platform | File |"
|
||||
echo "|-----------------|-------------|"
|
||||
echo "| Mac (Apple Silicon) | [ChatLab-${VERSION_NUMBER}-arm64.dmg](https://github.com/hellodigua/ChatLab/releases/download/v${VERSION_NUMBER}/ChatLab-${VERSION_NUMBER}-arm64.dmg) |"
|
||||
echo "| Mac (Intel) | [ChatLab-${VERSION_NUMBER}-x64.dmg](https://github.com/hellodigua/ChatLab/releases/download/v${VERSION_NUMBER}/ChatLab-${VERSION_NUMBER}-x64.dmg) |"
|
||||
echo "| Windows | [ChatLab-${VERSION_NUMBER}-setup.exe](https://github.com/hellodigua/ChatLab/releases/download/v${VERSION_NUMBER}/ChatLab-${VERSION_NUMBER}-setup.exe) |"
|
||||
echo "| Linux (AppImage) | [ChatLab-${VERSION_NUMBER}.AppImage](https://github.com/hellodigua/ChatLab/releases/download/v${VERSION_NUMBER}/ChatLab-${VERSION_NUMBER}.AppImage) |"
|
||||
echo "| Linux (deb, x86_64) | [chatlab_${VERSION_NUMBER}_amd64.deb](https://github.com/hellodigua/ChatLab/releases/download/v${VERSION_NUMBER}/chatlab_${VERSION_NUMBER}_amd64.deb) |"
|
||||
echo "| Linux (deb, ARM64) | [chatlab_${VERSION_NUMBER}_arm64.deb](https://github.com/hellodigua/ChatLab/releases/download/v${VERSION_NUMBER}/chatlab_${VERSION_NUMBER}_arm64.deb) |"
|
||||
echo "| Mac (Apple Silicon) | [ChatLab-${VERSION_NUMBER}-arm64.dmg](https://github.com/ChatLab/ChatLab/releases/download/v${VERSION_NUMBER}/ChatLab-${VERSION_NUMBER}-arm64.dmg) |"
|
||||
echo "| Mac (Intel) | [ChatLab-${VERSION_NUMBER}-x64.dmg](https://github.com/ChatLab/ChatLab/releases/download/v${VERSION_NUMBER}/ChatLab-${VERSION_NUMBER}-x64.dmg) |"
|
||||
echo "| Windows | [ChatLab-${VERSION_NUMBER}-setup.exe](https://github.com/ChatLab/ChatLab/releases/download/v${VERSION_NUMBER}/ChatLab-${VERSION_NUMBER}-setup.exe) |"
|
||||
echo "| Linux (AppImage) | [ChatLab-${VERSION_NUMBER}.AppImage](https://github.com/ChatLab/ChatLab/releases/download/v${VERSION_NUMBER}/ChatLab-${VERSION_NUMBER}.AppImage) |"
|
||||
echo "| Linux (deb, x86_64) | [chatlab_${VERSION_NUMBER}_amd64.deb](https://github.com/ChatLab/ChatLab/releases/download/v${VERSION_NUMBER}/chatlab_${VERSION_NUMBER}_amd64.deb) |"
|
||||
echo "| Linux (deb, ARM64) | [chatlab_${VERSION_NUMBER}_arm64.deb](https://github.com/ChatLab/ChatLab/releases/download/v${VERSION_NUMBER}/chatlab_${VERSION_NUMBER}_arm64.deb) |"
|
||||
} > release_notes.md
|
||||
|
||||
echo "Generated release notes:"
|
||||
|
||||
@@ -6,7 +6,7 @@ Rediscover your social memories with private, AI-powered analysis.
|
||||
|
||||
English | [简体中文](./docs/README.zh-CN.md) | [繁體中文](./docs/README.zh-TW.md) | [日本語](./docs/README.ja-JP.md)
|
||||
|
||||
[Official Website](https://chatlab.fun/) · [Download](https://chatlab.fun/?type=download) · [Documentation](https://chatlab.fun/usage/) · [Roadmap](https://chatlabfun.featurebase.app/roadmap) · [Issue Submission](https://github.com/hellodigua/ChatLab/issues)
|
||||
[Official Website](https://chatlab.fun/) · [Download](https://chatlab.fun/?type=download) · [Documentation](https://chatlab.fun/usage/) · [Roadmap](https://chatlabfun.featurebase.app/roadmap) · [Issue Submission](https://github.com/ChatLab/ChatLab/issues)
|
||||
|
||||
</div>
|
||||
|
||||
@@ -106,8 +106,8 @@ Please follow these principles before submitting a Pull Request:
|
||||
|
||||
Thanks to all contributors:
|
||||
|
||||
<a href="https://github.com/hellodigua/ChatLab/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=hellodigua/ChatLab" />
|
||||
<a href="https://github.com/ChatLab/ChatLab/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=ChatLab/ChatLab" />
|
||||
</a>
|
||||
|
||||
## License
|
||||
|
||||
@@ -6,7 +6,7 @@ AI Agent でチャット履歴をローカル分析し、あなたのソーシ
|
||||
|
||||
[English](../README.md) | [简体中文](./README.zh-CN.md) | [繁體中文](./README.zh-TW.md) | 日本語
|
||||
|
||||
[公式サイト](https://chatlab.fun/) · [ダウンロードガイド](https://chatlab.fun/?type=download) · [ドキュメント](https://chatlab.fun/usage/) · [Roadmap](https://chatlabfun.featurebase.app/roadmap) · [Issue](https://github.com/hellodigua/ChatLab/issues)
|
||||
[公式サイト](https://chatlab.fun/) · [ダウンロードガイド](https://chatlab.fun/?type=download) · [ドキュメント](https://chatlab.fun/usage/) · [Roadmap](https://chatlabfun.featurebase.app/roadmap) · [Issue](https://github.com/ChatLab/ChatLab/issues)
|
||||
|
||||
</div>
|
||||
|
||||
@@ -96,8 +96,8 @@ Pull Request を送る前に、次の方針を確認してください。
|
||||
|
||||
ChatLab に貢献してくださったすべての方に感謝します!
|
||||
|
||||
<a href="https://github.com/hellodigua/ChatLab/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=hellodigua/ChatLab" />
|
||||
<a href="https://github.com/ChatLab/ChatLab/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=ChatLab/ChatLab" />
|
||||
</a>
|
||||
|
||||
## License
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
[English](../README.md) | 简体中文 | [繁體中文](./README.zh-TW.md) | [日本語](./README.ja-JP.md)
|
||||
|
||||
[官网](https://chatlab.fun/cn/) · [下载指南](https://chatlab.fun/cn/?type=download) · [项目文档](https://chatlab.fun/cn/usage/) · [路线图](https://chatlabfun.featurebase.app/roadmap) · [问题提交](https://github.com/hellodigua/ChatLab/issues)
|
||||
[官网](https://chatlab.fun/cn/) · [下载指南](https://chatlab.fun/cn/?type=download) · [项目文档](https://chatlab.fun/cn/usage/) · [路线图](https://chatlabfun.featurebase.app/roadmap) · [问题提交](https://github.com/ChatLab/ChatLab/issues)
|
||||
|
||||
</div>
|
||||
|
||||
@@ -96,8 +96,8 @@ electron-fix start
|
||||
|
||||
感谢所有为 ChatLab 做出贡献的人!
|
||||
|
||||
<a href="https://github.com/hellodigua/ChatLab/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=hellodigua/ChatLab" />
|
||||
<a href="https://github.com/ChatLab/ChatLab/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=ChatLab/ChatLab" />
|
||||
</a>
|
||||
|
||||
## License
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
[English](../README.md) | [简体中文](./README.zh-CN.md) | 繁體中文 | [日本語](./README.ja-JP.md)
|
||||
|
||||
[官網](https://chatlab.fun/cn/) · [下載指南](https://chatlab.fun/cn/?type=download) · [使用文件](https://chatlab.fun/cn/usage/) · [Roadmap](https://chatlabfun.featurebase.app/roadmap) · [問題回報](https://github.com/hellodigua/ChatLab/issues)
|
||||
[官網](https://chatlab.fun/cn/) · [下載指南](https://chatlab.fun/cn/?type=download) · [使用文件](https://chatlab.fun/cn/usage/) · [Roadmap](https://chatlabfun.featurebase.app/roadmap) · [問題回報](https://github.com/ChatLab/ChatLab/issues)
|
||||
|
||||
</div>
|
||||
|
||||
@@ -96,8 +96,8 @@ electron-fix start
|
||||
|
||||
感謝所有為 ChatLab 做出貢獻的人!
|
||||
|
||||
<a href="https://github.com/hellodigua/ChatLab/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=hellodigua/ChatLab" />
|
||||
<a href="https://github.com/ChatLab/ChatLab/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=ChatLab/ChatLab" />
|
||||
</a>
|
||||
|
||||
## License
|
||||
|
||||
@@ -1,4 +1,26 @@
|
||||
[
|
||||
{
|
||||
"version": "0.18.1",
|
||||
"date": "2026-04-24",
|
||||
"summary": "新增 DeepSeek V4 支持,新增开机自启动选项,优化设置与界面体验,并修复 AI 对话链接打开方式。<br/>从本版本开始,项目将正式迁移至ChatLab组织。",
|
||||
"changes": [
|
||||
{
|
||||
"type": "feat",
|
||||
"items": [
|
||||
"迁移至 ChatLab 组织",
|
||||
"优化全局样式表现",
|
||||
"优化快速提问展示逻辑",
|
||||
"优化设置弹窗弹出方式",
|
||||
"支持开机自启动",
|
||||
"支持 DeepSeek V4 模型"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "fix",
|
||||
"items": ["修复 AI 对话中的链接未在浏览器中打开的问题"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "0.18.0",
|
||||
"date": "2026-04-23",
|
||||
|
||||
@@ -1,4 +1,26 @@
|
||||
[
|
||||
{
|
||||
"version": "0.18.1",
|
||||
"date": "2026-04-24",
|
||||
"summary": "This release adds DeepSeek V4 support and a launch-at-startup option, improves the settings and overall UI experience, and fixes how links open in AI chat.<br/>Starting with this release, the project is officially moving to the ChatLab organization.",
|
||||
"changes": [
|
||||
{
|
||||
"type": "feat",
|
||||
"items": [
|
||||
"Move the project to the ChatLab organization.",
|
||||
"Refine the global visual style.",
|
||||
"Improve how quick prompts are presented.",
|
||||
"Improve how the settings modal opens.",
|
||||
"Add launch-at-startup support.",
|
||||
"Add support for the DeepSeek V4 model."
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "fix",
|
||||
"items": ["Fix links in AI chat so they open in the browser."]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "0.18.0",
|
||||
"date": "2026-04-23",
|
||||
|
||||
@@ -1,4 +1,26 @@
|
||||
[
|
||||
{
|
||||
"version": "0.18.1",
|
||||
"date": "2026-04-24",
|
||||
"summary": "DeepSeek V4 への対応と自動起動オプションを追加し、設定まわりと全体の UI 体験を改善しました。あわせて、AI チャット内リンクの開き方も修正しています。<br/>このバージョンから、プロジェクトは正式に ChatLab 組織へ移行します。",
|
||||
"changes": [
|
||||
{
|
||||
"type": "feat",
|
||||
"items": [
|
||||
"プロジェクトを ChatLab 組織へ移行",
|
||||
"全体のスタイル表現を改善",
|
||||
"クイック質問の表示ロジックを改善",
|
||||
"設定モーダルの開き方を改善",
|
||||
"自動起動に対応",
|
||||
"DeepSeek V4 モデルに対応"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "fix",
|
||||
"items": ["AI チャット内のリンクがブラウザで開かない問題を修正"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "0.18.0",
|
||||
"date": "2026-04-23",
|
||||
|
||||
@@ -1,4 +1,26 @@
|
||||
[
|
||||
{
|
||||
"version": "0.18.1",
|
||||
"date": "2026-04-24",
|
||||
"summary": "本次更新新增 DeepSeek V4 支援與開機自動啟動選項,優化設定與整體介面體驗,並修正 AI 對話中的連結開啟方式。<br/>從這個版本開始,專案將正式遷移至 ChatLab 組織。",
|
||||
"changes": [
|
||||
{
|
||||
"type": "feat",
|
||||
"items": [
|
||||
"遷移至 ChatLab 組織",
|
||||
"優化全域樣式表現",
|
||||
"優化快速提問的顯示邏輯",
|
||||
"優化設定彈窗的開啟方式",
|
||||
"支援開機自動啟動",
|
||||
"支援 DeepSeek V4 模型"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "fix",
|
||||
"items": ["修正 AI 對話中的連結未在瀏覽器中開啟的問題"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "0.18.0",
|
||||
"date": "2026-04-23",
|
||||
|
||||
@@ -5,7 +5,7 @@ productName: ChatLab
|
||||
# 发布配置
|
||||
publish:
|
||||
provider: github
|
||||
owner: hellodigua
|
||||
owner: ChatLab
|
||||
repo: ChatLab
|
||||
# 构建资源所在的目录
|
||||
directories:
|
||||
|
||||
@@ -301,20 +301,20 @@ const GEMINI_MODELS: ModelDefinition[] = [
|
||||
|
||||
const DEEPSEEK_MODELS: ModelDefinition[] = [
|
||||
builtin({
|
||||
id: 'deepseek-chat',
|
||||
id: 'deepseek-v4-pro',
|
||||
providerId: 'deepseek',
|
||||
name: 'DeepSeek Chat',
|
||||
description: 'General chat model (V3)',
|
||||
capabilities: ['chat', 'function_calling'],
|
||||
name: 'DeepSeek V4 Pro',
|
||||
description: 'Flagship model, 1M context, thinking & non-thinking',
|
||||
capabilities: ['chat', 'reasoning', 'function_calling'],
|
||||
recommendedFor: ['chat'],
|
||||
status: 'stable',
|
||||
}),
|
||||
builtin({
|
||||
id: 'deepseek-reasoner',
|
||||
id: 'deepseek-v4-flash',
|
||||
providerId: 'deepseek',
|
||||
name: 'DeepSeek Reasoner',
|
||||
description: 'Deep reasoning model (R1)',
|
||||
capabilities: ['chat', 'reasoning'],
|
||||
name: 'DeepSeek V4 Flash',
|
||||
description: 'Fast model, 1M context, thinking & non-thinking',
|
||||
capabilities: ['chat', 'reasoning', 'function_calling'],
|
||||
recommendedFor: ['chat'],
|
||||
status: 'stable',
|
||||
}),
|
||||
@@ -669,11 +669,11 @@ const GROQ_MODELS: ModelDefinition[] = [
|
||||
|
||||
const OPENROUTER_MODELS: ModelDefinition[] = [
|
||||
builtin({
|
||||
id: 'deepseek/deepseek-chat',
|
||||
id: 'deepseek/deepseek-v4-pro',
|
||||
providerId: 'openrouter',
|
||||
name: 'DeepSeek Chat',
|
||||
description: 'DeepSeek V3 via OpenRouter',
|
||||
capabilities: ['chat', 'function_calling'],
|
||||
name: 'DeepSeek V4 Pro',
|
||||
description: 'DeepSeek V4 Pro via OpenRouter',
|
||||
capabilities: ['chat', 'reasoning', 'function_calling'],
|
||||
recommendedFor: ['chat'],
|
||||
status: 'stable',
|
||||
}),
|
||||
|
||||
@@ -92,7 +92,7 @@ const DEEPSEEK: ProviderDefinition = {
|
||||
supportsCustomModels: true,
|
||||
builtin: true,
|
||||
enabledByDefault: true,
|
||||
modelIds: ['deepseek-chat', 'deepseek-reasoner'],
|
||||
modelIds: ['deepseek-v4-pro', 'deepseek-v4-flash'],
|
||||
}
|
||||
|
||||
const QWEN: ProviderDefinition = {
|
||||
@@ -216,7 +216,7 @@ const OPENROUTER: ProviderDefinition = {
|
||||
supportsCustomModels: true,
|
||||
builtin: true,
|
||||
enabledByDefault: true,
|
||||
modelIds: ['deepseek/deepseek-chat', 'google/gemini-2.5-flash-preview', 'mistralai/mistral-7b-instruct:free'],
|
||||
modelIds: ['deepseek/deepseek-v4-pro', 'google/gemini-2.5-flash-preview', 'mistralai/mistral-7b-instruct:free'],
|
||||
}
|
||||
|
||||
const XAI: ProviderDefinition = {
|
||||
|
||||
@@ -199,6 +199,23 @@ export function registerWindowHandlers(ctx: IpcContext): void {
|
||||
}
|
||||
})
|
||||
|
||||
// ==================== 开机自启动 ====================
|
||||
ipcMain.handle('app:getOpenAtLogin', () => {
|
||||
if (!app.isPackaged) return false
|
||||
const { openAtLogin } = app.getLoginItemSettings()
|
||||
return openAtLogin
|
||||
})
|
||||
|
||||
ipcMain.handle('app:setOpenAtLogin', (_, enabled: boolean) => {
|
||||
if (!app.isPackaged) return { success: false, error: 'Not available in dev mode' }
|
||||
try {
|
||||
app.setLoginItemSettings({ openAtLogin: enabled })
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
return { success: false, error: String(error) }
|
||||
}
|
||||
})
|
||||
|
||||
// ==================== 更新检查 ====================
|
||||
ipcMain.on('check-update', () => {
|
||||
// 手动检查更新(即使是预发布版本也会提示)
|
||||
|
||||
@@ -59,7 +59,7 @@ function switchToGitHub(): void {
|
||||
currentSource = 'github'
|
||||
autoUpdater.setFeedURL({
|
||||
provider: 'github',
|
||||
owner: 'hellodigua',
|
||||
owner: 'ChatLab',
|
||||
repo: 'ChatLab',
|
||||
})
|
||||
logger.info('[Update] Switched to GitHub fallback source')
|
||||
|
||||
@@ -102,5 +102,17 @@ export const extendedApi = {
|
||||
relaunch: (): Promise<void> => {
|
||||
return ipcRenderer.invoke('app:relaunch')
|
||||
},
|
||||
/**
|
||||
* 获取开机自启动状态
|
||||
*/
|
||||
getOpenAtLogin: (): Promise<boolean> => {
|
||||
return ipcRenderer.invoke('app:getOpenAtLogin')
|
||||
},
|
||||
/**
|
||||
* 设置开机自启动
|
||||
*/
|
||||
setOpenAtLogin: (enabled: boolean): Promise<{ success: boolean; error?: string }> => {
|
||||
return ipcRenderer.invoke('app:setOpenAtLogin', enabled)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
Vendored
+2
@@ -213,6 +213,8 @@ interface Api {
|
||||
getAnalyticsEnabled: () => Promise<boolean>
|
||||
setAnalyticsEnabled: (enabled: boolean) => Promise<{ success: boolean }>
|
||||
relaunch: () => Promise<void>
|
||||
getOpenAtLogin: () => Promise<boolean>
|
||||
setOpenAtLogin: (enabled: boolean) => Promise<{ success: boolean; error?: string }>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"name": "ChatLab",
|
||||
"version": "0.18.0",
|
||||
"version": "0.18.1",
|
||||
"description": "本地化的聊天记录分析工具,通过 SQL 和 AI Agent 回顾你的社交记忆",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/hellodigua/ChatLab.git"
|
||||
"url": "git+https://github.com/ChatLab/ChatLab.git"
|
||||
},
|
||||
"author": "digua <hello@digua.me>",
|
||||
"main": "./out/main/index.js",
|
||||
|
||||
+21
-1
@@ -1,11 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue'
|
||||
import { onMounted, onUnmounted } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import TitleBar from '@/components/common/TitleBar.vue'
|
||||
import Sidebar from '@/components/common/Sidebar.vue'
|
||||
import ScreenCaptureModal from '@/components/common/ScreenCaptureModal.vue'
|
||||
import SettingsModal from '@/components/common/SettingsModal.vue'
|
||||
import { ChatRecordDrawer } from '@/components/common/ChatRecord'
|
||||
import GlobalTaskBar from '@/components/AIChat/GlobalTaskBar.vue'
|
||||
import { useSessionStore } from '@/stores/session'
|
||||
@@ -32,8 +33,21 @@ const toaster = {
|
||||
duration: 2000,
|
||||
}
|
||||
|
||||
// Cmd+, (macOS) / Ctrl+, (Windows/Linux) 打开设置
|
||||
function handleGlobalKeydown(e: KeyboardEvent) {
|
||||
const isMeta = navigator.platform.toLowerCase().includes('mac') ? e.metaKey : e.ctrlKey
|
||||
if (isMeta && e.key === ',') {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
if (!layoutStore.showSettings) {
|
||||
layoutStore.openSettings()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 应用启动时初始化
|
||||
onMounted(async () => {
|
||||
window.addEventListener('keydown', handleGlobalKeydown)
|
||||
// 平台检测 - 设置 CSS 类名以驱动平台差异化样式(如标题栏安全区域高度)
|
||||
const platform = navigator.platform.toLowerCase()
|
||||
if (platform.includes('win')) {
|
||||
@@ -49,6 +63,10 @@ onMounted(async () => {
|
||||
// 从数据库加载会话列表
|
||||
await sessionStore.loadSessions()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('keydown', handleGlobalKeydown)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -81,6 +99,8 @@ onMounted(async () => {
|
||||
:image-data="layoutStore.screenCaptureImage"
|
||||
@update:open="(v) => (v ? null : layoutStore.closeScreenCaptureModal())"
|
||||
/>
|
||||
<!-- 全局设置弹窗 -->
|
||||
<SettingsModal />
|
||||
<!-- 全局聊天记录查看器 -->
|
||||
<ChatRecordDrawer />
|
||||
<!-- 全局 AI 后台任务条:允许用户离开当前页面后仍然快速返回进行中的对话。 -->
|
||||
|
||||
@@ -27,7 +27,7 @@ Chat histories do not contain only your data. They also include what the other p
|
||||
|
||||
## 4. Risk Warning
|
||||
|
||||
- **Official Channels**: Please download this software only from [chatlab.fun](https://chatlab.fun) or [GitHub Releases](https://github.com/hellodigua/ChatLab/releases).
|
||||
- **Official Channels**: Please download this software only from [chatlab.fun](https://chatlab.fun) or [GitHub Releases](https://github.com/ChatLab/ChatLab/releases).
|
||||
- **Supply Chain Risk**: This project is fully open-source, so anyone can repackage it. Unofficial versions shared by others may contain malicious code that could expose your API tokens, chat records, or local data. Please stay vigilant.
|
||||
- **Accuracy of Results**: Analysis results generated by the software or AI may contain errors or hallucinations. They are for reference only and should not be used as legal evidence or as the basis for formal decisions.
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ ChatLab へようこそ。ChatLab は、**オープンソースかつローカ
|
||||
|
||||
## 4. 注意事項
|
||||
|
||||
- **公式配布元のみ利用してください**:本ソフトウェアは [chatlab.fun](https://chatlab.fun) または [GitHub Releases](https://github.com/hellodigua/ChatLab/releases) からのみ入手してください。
|
||||
- **公式配布元のみ利用してください**:本ソフトウェアは [chatlab.fun](https://chatlab.fun) または [GitHub Releases](https://github.com/ChatLab/ChatLab/releases) からのみ入手してください。
|
||||
- **サプライチェーンリスクに注意してください**:本プロジェクトは完全オープンソースのため、第三者が再配布版を作成できます。非公式版には悪意あるコードが含まれる可能性があり、API トークン、チャット履歴、ローカルデータの漏えいにつながるおそれがあります。
|
||||
- **結果の正確性は保証されません**:ソフトウェアや AI が生成する分析結果には、誤りやハルシネーションが含まれる可能性があります。参考情報として扱い、法的証拠や正式な判断材料には使用しないでください。
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
|
||||
## 4. 风险警告
|
||||
|
||||
- **官方渠道**:请仅通过 [chatlab.fun](https://chatlab.fun) 或 [GitHub Release](https://github.com/hellodigua/ChatLab/releases) 下载本软件。
|
||||
- **官方渠道**:请仅通过 [chatlab.fun](https://chatlab.fun) 或 [GitHub Release](https://github.com/ChatLab/ChatLab/releases) 下载本软件。
|
||||
- **供应链风险**:本项目代码完全开源,任何人均可二次打包。他人分享的非官方版本可能会植入恶意代码,导致你的 API Token、聊天记录或本地数据泄露,请务必保持警惕。
|
||||
- **结果准确性**:软件和 AI 生成的分析结果可能存在错误或"幻觉",仅供参考,不应作为法律证据或正式决策依据。
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
|
||||
## 4. 風險警告
|
||||
|
||||
- **官方下載管道**:請僅透過 [chatlab.fun](https://chatlab.fun) 或 [GitHub Releases](https://github.com/hellodigua/ChatLab/releases) 下載本軟體。
|
||||
- **官方下載管道**:請僅透過 [chatlab.fun](https://chatlab.fun) 或 [GitHub Releases](https://github.com/ChatLab/ChatLab/releases) 下載本軟體。
|
||||
- **供應鏈風險**:本專案程式碼完全開源,任何人都可再次打包。非官方版本可能植入惡意程式碼,導致你的 API Token、聊天紀錄或本機資料外洩,請務必提高警覺。
|
||||
- **結果準確性**:軟體與 AI 產生的分析結果可能有誤或出現「幻覺」,僅供參考,不應作為法律證據或正式決策依據。
|
||||
|
||||
|
||||
@@ -103,6 +103,7 @@ const currentChatType = computed(() => props.chatType ?? 'group')
|
||||
const isSourcePanelCollapsed = ref(false)
|
||||
const hasLLMConfig = ref(false)
|
||||
const isCheckingConfig = ref(true)
|
||||
const configModalScrollToSection = ref<string | undefined>(undefined)
|
||||
const conversationListRef = ref<InstanceType<typeof ConversationList> | null>(null)
|
||||
const chatInputRef = ref<{
|
||||
fillInput: (content: string) => void
|
||||
@@ -185,6 +186,20 @@ async function handlePresetQuestion(question: string) {
|
||||
}
|
||||
}
|
||||
|
||||
function handleEditPresetQuestions() {
|
||||
const id = assistantStore.selectedAssistant?.id
|
||||
if (!id) return
|
||||
configModalScrollToSection.value = 'presetQuestions'
|
||||
handleConfigureAssistant(id)
|
||||
}
|
||||
|
||||
function handleConfigModalOpenUpdate(value: boolean) {
|
||||
configModalVisible.value = value
|
||||
if (!value) {
|
||||
configModalScrollToSection.value = undefined
|
||||
}
|
||||
}
|
||||
|
||||
function handleUseSkillEntry() {
|
||||
chatInputRef.value?.openSkillSelector()
|
||||
}
|
||||
@@ -332,15 +347,23 @@ watch(
|
||||
</h2>
|
||||
|
||||
<!-- 系统提示词文本 -->
|
||||
<p
|
||||
v-if="showWelcomeCard && welcomeInfo.name"
|
||||
class="mb-8 max-w-lg cursor-pointer text-center text-sm leading-relaxed text-gray-500 transition-colors hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 line-clamp-2"
|
||||
@click="handleConfigureAssistant(assistantStore.selectedAssistant!.id)"
|
||||
>
|
||||
<UTooltip :text="t('ai.assistant.systemPrompt', '系统设定')" :popper="{ placement: 'top' }">
|
||||
{{ welcomeInfo.preview }}
|
||||
</UTooltip>
|
||||
</p>
|
||||
<div v-if="showWelcomeCard && welcomeInfo.name" class="relative mb-8 w-full max-w-lg">
|
||||
<p
|
||||
class="cursor-pointer pr-7 text-center text-sm leading-relaxed text-gray-500 transition-colors hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 line-clamp-2"
|
||||
@click="handleConfigureAssistant(assistantStore.selectedAssistant!.id)"
|
||||
>
|
||||
<UTooltip :text="t('ai.assistant.systemPrompt', '系统设定')" :popper="{ placement: 'top' }">
|
||||
{{ welcomeInfo.preview }}
|
||||
</UTooltip>
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
class="absolute bottom-0 right-0 rounded-md p-0.5 text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-600 dark:hover:bg-gray-800 dark:hover:text-gray-200"
|
||||
@click.stop="handleConfigureAssistant(assistantStore.selectedAssistant!.id)"
|
||||
>
|
||||
<UIcon name="i-heroicons-pencil-square" class="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 助手选择器 -->
|
||||
<div class="flex w-full justify-center">
|
||||
@@ -426,6 +449,7 @@ watch(
|
||||
:leading-action-label="t('ai.chat.input.useSkill')"
|
||||
@select="handlePresetQuestion"
|
||||
@leading-action="handleUseSkillEntry"
|
||||
@edit-questions="handleEditPresetQuestions"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -480,7 +504,8 @@ watch(
|
||||
:open="configModalVisible"
|
||||
:assistant-id="configModalAssistantId"
|
||||
:readonly="configModalReadonly"
|
||||
@update:open="configModalVisible = $event"
|
||||
:scroll-to-section="configModalScrollToSection"
|
||||
@update:open="handleConfigModalOpenUpdate"
|
||||
@saved="handleAssistantConfigSaved"
|
||||
@created="handleAssistantCreated"
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, computed, onMounted } from 'vue'
|
||||
import { ref, watch, computed, onMounted, nextTick } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { useAssistantStore, type AssistantConfigFull } from '@/stores/assistant'
|
||||
@@ -10,6 +10,7 @@ const props = defineProps<{
|
||||
open: boolean
|
||||
assistantId: string | null
|
||||
readonly?: boolean
|
||||
scrollToSection?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -61,6 +62,7 @@ const form = ref({
|
||||
})
|
||||
|
||||
const newQuestion = ref('')
|
||||
const presetQuestionsRef = ref<HTMLElement | null>(null)
|
||||
|
||||
const toolBadgeCount = computed(() => form.value.allowedBuiltinTools.length)
|
||||
|
||||
@@ -82,6 +84,12 @@ watch(
|
||||
isCreateMode.value = true
|
||||
initEmptyForm()
|
||||
}
|
||||
if (props.scrollToSection === 'presetQuestions') {
|
||||
await nextTick()
|
||||
setTimeout(() => {
|
||||
presetQuestionsRef.value?.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||
}, 100)
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
@@ -381,7 +389,7 @@ function closeModal() {
|
||||
<p class="mt-1 text-[10px] text-gray-400">{{ t('ai.assistant.config.localeHint') }}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div ref="presetQuestionsRef">
|
||||
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ t('ai.assistant.config.presetQuestions') }}
|
||||
</label>
|
||||
|
||||
@@ -38,6 +38,12 @@ const md = new MarkdownIt({
|
||||
typographer: true, // 启用排版优化
|
||||
})
|
||||
|
||||
md.renderer.rules.link_open = (tokens, idx, options, _env, self) => {
|
||||
tokens[idx].attrSet('target', '_blank')
|
||||
tokens[idx].attrSet('rel', 'noopener noreferrer')
|
||||
return self.renderToken(tokens, idx, options)
|
||||
}
|
||||
|
||||
// 渲染 Markdown 文本
|
||||
function renderMarkdown(text: string): string {
|
||||
if (!text) return ''
|
||||
|
||||
@@ -3,15 +3,15 @@ import { ref, computed } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { usePromptStore } from '@/stores/prompt'
|
||||
import { useLayoutStore } from '@/stores/layout'
|
||||
import { useLLMStore } from '@/stores/llm'
|
||||
import { exportConversation, type ExportFormat } from '@/utils/conversationExport'
|
||||
import type { AgentRuntimeStatus } from '@electron/shared/types'
|
||||
|
||||
const { t } = useI18n()
|
||||
const toast = useToast()
|
||||
const router = useRouter()
|
||||
const layoutStore = useLayoutStore()
|
||||
|
||||
// Props
|
||||
const props = defineProps<{
|
||||
@@ -86,7 +86,7 @@ const agentCompactTitle = computed(() => {
|
||||
})
|
||||
|
||||
function openChatSettings() {
|
||||
router.push({ name: 'settings', query: { tab: 'ai', subTab: 'chat' } })
|
||||
layoutStore.openSettings('ai', 'chat')
|
||||
}
|
||||
|
||||
// 切换 AI 模型配置
|
||||
@@ -101,7 +101,7 @@ async function switchModelConfig(configId: string) {
|
||||
|
||||
function openModelSettings() {
|
||||
isModelPopoverOpen.value = false
|
||||
router.push({ name: 'settings', query: { tab: 'ai', subTab: 'model' } })
|
||||
layoutStore.openSettings('ai', 'model')
|
||||
}
|
||||
|
||||
// 导出当前对话
|
||||
@@ -301,7 +301,7 @@ async function openAiLogFile() {
|
||||
:disabled="isOpeningLog"
|
||||
@click="openAiLogFile"
|
||||
>
|
||||
<UIcon name="i-heroicons-folder-open" class="h-3.5 w-3.5" />
|
||||
<UIcon name="i-heroicons-document-text" class="h-3.5 w-3.5" />
|
||||
<span class="hidden xl:inline">{{ t('ai.chat.statusBar.log.label') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { ref, onBeforeUnmount } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
@@ -13,212 +13,99 @@ const props = defineProps<{
|
||||
const emit = defineEmits<{
|
||||
select: [question: string]
|
||||
leadingAction: []
|
||||
editQuestions: []
|
||||
}>()
|
||||
|
||||
type PresetItem =
|
||||
| {
|
||||
key: string
|
||||
label: string
|
||||
type: 'leadingAction'
|
||||
}
|
||||
| {
|
||||
key: string
|
||||
label: string
|
||||
type: 'question'
|
||||
question: string
|
||||
}
|
||||
|
||||
const hasItems = computed(() => props.questions.length > 0 || Boolean(props.leadingActionLabel))
|
||||
const containerRef = ref<HTMLElement | null>(null)
|
||||
const measureRef = ref<HTMLElement | null>(null)
|
||||
const showMoreMenu = ref(false)
|
||||
const visibleCount = ref(0)
|
||||
let resizeObserver: ResizeObserver | null = null
|
||||
const showPanel = ref(false)
|
||||
let hideTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
const chipClass =
|
||||
'rounded-full ring-1 ring-inset ring-gray-200/80 bg-white/60 shadow-[0_2px_8px_-2px_rgba(0,0,0,0.05)] backdrop-blur-md px-3 py-1.5 text-xs text-gray-600 transition-all duration-300 hover:-translate-y-[1px] hover:ring-primary-300 hover:bg-white/90 hover:text-primary-600 hover:shadow-[0_4px_12px_-2px_rgba(0,0,0,0.08)] disabled:cursor-not-allowed disabled:opacity-50 dark:ring-gray-700/60 dark:bg-gray-800/60 dark:text-gray-300 dark:hover:ring-primary-500/50 dark:hover:bg-gray-800/90 dark:hover:text-primary-400'
|
||||
|
||||
const items = computed<PresetItem[]>(() => {
|
||||
const result: PresetItem[] = []
|
||||
|
||||
if (props.leadingActionLabel) {
|
||||
result.push({
|
||||
key: 'leading-action',
|
||||
label: props.leadingActionLabel,
|
||||
type: 'leadingAction',
|
||||
})
|
||||
}
|
||||
|
||||
props.questions.forEach((question, index) => {
|
||||
result.push({
|
||||
key: `question-${index}`,
|
||||
label: question,
|
||||
type: 'question',
|
||||
question,
|
||||
})
|
||||
})
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
const hiddenCount = computed(() => Math.max(items.value.length - visibleCount.value, 0))
|
||||
const visibleItems = computed(() => items.value.slice(0, visibleCount.value))
|
||||
const hiddenItems = computed(() => items.value.slice(visibleCount.value))
|
||||
const measureMoreLabel = computed(() => `${t('ai.chat.input.presetMore')}...`)
|
||||
const toggleLabel = computed(() => `${t('ai.chat.input.presetMore')}...`)
|
||||
|
||||
function handleItemClick(item: PresetItem) {
|
||||
showMoreMenu.value = false
|
||||
|
||||
if (item.type === 'leadingAction') {
|
||||
emit('leadingAction')
|
||||
return
|
||||
}
|
||||
|
||||
emit('select', item.question)
|
||||
function handleSelectQuestion(question: string) {
|
||||
showPanel.value = false
|
||||
emit('select', question)
|
||||
}
|
||||
|
||||
function toggleMoreMenu() {
|
||||
if (props.disabled || hiddenCount.value === 0) return
|
||||
showMoreMenu.value = !showMoreMenu.value
|
||||
function openPanel() {
|
||||
if (props.disabled || props.questions.length === 0) return
|
||||
if (hideTimer) {
|
||||
clearTimeout(hideTimer)
|
||||
hideTimer = null
|
||||
}
|
||||
showPanel.value = true
|
||||
}
|
||||
|
||||
function handleDocumentMouseDown(event: MouseEvent) {
|
||||
if (!showMoreMenu.value || !containerRef.value) return
|
||||
|
||||
const target = event.target
|
||||
if (target instanceof Node && !containerRef.value.contains(target)) {
|
||||
showMoreMenu.value = false
|
||||
}
|
||||
function scheduleClose() {
|
||||
hideTimer = setTimeout(() => {
|
||||
showPanel.value = false
|
||||
}, 150)
|
||||
}
|
||||
|
||||
function measureVisibleItems() {
|
||||
if (!containerRef.value) return
|
||||
|
||||
const chips = Array.from(measureRef.value?.querySelectorAll<HTMLElement>('[data-measure-chip]') ?? [])
|
||||
if (chips.length === 0) {
|
||||
visibleCount.value = 0
|
||||
return
|
||||
}
|
||||
|
||||
const availableWidth = containerRef.value.clientWidth
|
||||
if (!availableWidth) {
|
||||
visibleCount.value = chips.length
|
||||
return
|
||||
}
|
||||
|
||||
const moreButton = measureRef.value?.querySelector<HTMLElement>('[data-measure-more]')
|
||||
const chipWidths = chips.map((chip) => chip.offsetWidth)
|
||||
const moreWidth = moreButton?.offsetWidth ?? 0
|
||||
const gap = 8
|
||||
|
||||
let usedWidth = 0
|
||||
let nextVisibleCount = 0
|
||||
|
||||
for (let index = 0; index < chipWidths.length; index += 1) {
|
||||
const width = chipWidths[index]
|
||||
const nextWidth = nextVisibleCount === 0 ? width : usedWidth + gap + width
|
||||
const hasRemaining = index < chipWidths.length - 1
|
||||
const reserveWidth = hasRemaining ? gap + moreWidth : 0
|
||||
|
||||
if (nextWidth + reserveWidth > availableWidth) {
|
||||
break
|
||||
}
|
||||
|
||||
usedWidth = nextWidth
|
||||
nextVisibleCount += 1
|
||||
}
|
||||
|
||||
// 至少展示一个标签,避免窄窗口下直接只剩“更多”。
|
||||
visibleCount.value = Math.max(1, nextVisibleCount)
|
||||
function handleEditQuestions() {
|
||||
showPanel.value = false
|
||||
emit('editQuestions')
|
||||
}
|
||||
|
||||
async function syncCollapsedLayout() {
|
||||
if (!hasItems.value) return
|
||||
|
||||
await nextTick()
|
||||
measureVisibleItems()
|
||||
}
|
||||
|
||||
watch(
|
||||
items,
|
||||
async () => {
|
||||
showMoreMenu.value = false
|
||||
await syncCollapsedLayout()
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
)
|
||||
|
||||
watch(hiddenCount, (count) => {
|
||||
if (count === 0) {
|
||||
showMoreMenu.value = false
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
await syncCollapsedLayout()
|
||||
document.addEventListener('mousedown', handleDocumentMouseDown)
|
||||
|
||||
if (typeof ResizeObserver === 'undefined' || !containerRef.value) return
|
||||
|
||||
resizeObserver = new ResizeObserver(() => {
|
||||
showMoreMenu.value = false
|
||||
measureVisibleItems()
|
||||
})
|
||||
resizeObserver.observe(containerRef.value)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
resizeObserver?.disconnect()
|
||||
document.removeEventListener('mousedown', handleDocumentMouseDown)
|
||||
if (hideTimer) clearTimeout(hideTimer)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="hasItems" ref="containerRef" class="relative">
|
||||
<div class="pointer-events-none absolute left-0 top-0 -z-10 h-0 overflow-hidden opacity-0" aria-hidden="true">
|
||||
<div ref="measureRef" class="flex whitespace-nowrap gap-2">
|
||||
<span v-for="item in items" :key="item.key" :class="chipClass" data-measure-chip>
|
||||
{{ item.label }}
|
||||
</span>
|
||||
<span :class="chipClass" data-measure-more>
|
||||
{{ measureMoreLabel }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主行保持单行展示,但不裁掉“更多”的上浮面板。 -->
|
||||
<div v-if="leadingActionLabel || questions.length > 0" class="relative">
|
||||
<div class="relative z-10 flex flex-nowrap gap-2">
|
||||
<button
|
||||
v-for="item in visibleItems"
|
||||
:key="item.key"
|
||||
:class="chipClass"
|
||||
:disabled="props.disabled"
|
||||
@click="handleItemClick(item)"
|
||||
>
|
||||
{{ item.label }}
|
||||
<button v-if="leadingActionLabel" :class="chipClass" :disabled="props.disabled" @click="emit('leadingAction')">
|
||||
{{ leadingActionLabel }}
|
||||
</button>
|
||||
|
||||
<div v-if="hiddenCount > 0" class="relative shrink-0">
|
||||
<button :class="chipClass" :disabled="props.disabled" @click="toggleMoreMenu">
|
||||
{{ toggleLabel }}
|
||||
<div v-if="questions.length > 0" class="relative shrink-0" @mouseenter="openPanel" @mouseleave="scheduleClose">
|
||||
<button
|
||||
:class="[
|
||||
chipClass,
|
||||
showPanel
|
||||
? 'ring-primary-300 bg-white/90 text-primary-600 dark:ring-primary-500/50 dark:bg-gray-800/90 dark:text-primary-400'
|
||||
: '',
|
||||
]"
|
||||
:disabled="props.disabled"
|
||||
>
|
||||
<span class="inline-flex items-center gap-1">
|
||||
<UIcon name="i-heroicons-chat-bubble-left-ellipsis" class="h-3.5 w-3.5" />
|
||||
{{ t('ai.chat.input.quickAsk') }}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<!-- 隐藏标签直接向上展开,并与主行保持同一套胶囊样式。 -->
|
||||
<div
|
||||
v-if="showMoreMenu"
|
||||
class="absolute right-0 bottom-full z-20 mb-2 flex w-[320px] max-w-[calc(100vw-3rem)] flex-wrap justify-end gap-2"
|
||||
<Transition
|
||||
enter-active-class="transition duration-200 ease-out"
|
||||
enter-from-class="opacity-0 translate-y-2"
|
||||
enter-to-class="opacity-100 translate-y-0"
|
||||
leave-active-class="transition duration-150 ease-in"
|
||||
leave-from-class="opacity-100 translate-y-0"
|
||||
leave-to-class="opacity-0 translate-y-2"
|
||||
>
|
||||
<button
|
||||
v-for="item in hiddenItems"
|
||||
:key="item.key"
|
||||
:class="chipClass"
|
||||
:disabled="props.disabled"
|
||||
@click="handleItemClick(item)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="showPanel" class="absolute left-0 bottom-full z-20 mb-2 w-[320px] max-w-[calc(100vw-3rem)]">
|
||||
<div class="mb-1.5 flex justify-start">
|
||||
<button
|
||||
class="inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[10px] text-gray-400 transition-colors hover:text-primary-500 dark:text-gray-500 dark:hover:text-primary-400"
|
||||
@click="handleEditQuestions"
|
||||
>
|
||||
<UIcon name="i-heroicons-pencil-square" class="h-3 w-3" />
|
||||
{{ t('ai.chat.input.editQuickAsk') }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
v-for="(question, index) in questions"
|
||||
:key="index"
|
||||
:class="chipClass"
|
||||
:disabled="props.disabled"
|
||||
@click="handleSelectQuestion(question)"
|
||||
>
|
||||
{{ question }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+15
-3
@@ -67,7 +67,11 @@ function closeModal() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UModal :open="open" :ui="{ content: 'max-w-2xl' }" @update:open="emit('update:open', $event)">
|
||||
<UModal
|
||||
:open="open"
|
||||
:ui="{ content: 'max-w-2xl z-[101]', overlay: 'z-[100]' }"
|
||||
@update:open="emit('update:open', $event)"
|
||||
>
|
||||
<template #content>
|
||||
<div class="flex min-h-[min(640px,80vh)] max-h-[80vh] flex-col p-6">
|
||||
<h3 class="mb-4 text-lg font-semibold text-gray-900 dark:text-white">{{ modalTitle }}</h3>
|
||||
@@ -419,7 +423,11 @@ function closeModal() {
|
||||
</UModal>
|
||||
|
||||
<!-- 验证失败确认弹窗 -->
|
||||
<UModal :open="showValidationFailConfirm" @update:open="showValidationFailConfirm = $event">
|
||||
<UModal
|
||||
:open="showValidationFailConfirm"
|
||||
:ui="{ content: 'z-[102]', overlay: 'z-[101]' }"
|
||||
@update:open="showValidationFailConfirm = $event"
|
||||
>
|
||||
<template #content>
|
||||
<div class="p-6">
|
||||
<div class="mb-4 flex items-start gap-3">
|
||||
@@ -445,7 +453,11 @@ function closeModal() {
|
||||
</UModal>
|
||||
|
||||
<!-- 添加自定义模型小弹窗 -->
|
||||
<UModal :open="showAddModelDialog" @update:open="showAddModelDialog = $event">
|
||||
<UModal
|
||||
:open="showAddModelDialog"
|
||||
:ui="{ content: 'z-[102]', overlay: 'z-[101]' }"
|
||||
@update:open="showAddModelDialog = $event"
|
||||
>
|
||||
<template #content>
|
||||
<div class="p-5">
|
||||
<h4 class="mb-4 text-base font-semibold text-gray-900 dark:text-white">
|
||||
+1
-1
@@ -133,7 +133,7 @@ function formatMessageCount(count?: number): string {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UModal :open="open" @update:open="emit('update:open', $event)">
|
||||
<UModal :open="open" :ui="{ content: 'z-[101]', overlay: 'z-[100]' }" @update:open="emit('update:open', $event)">
|
||||
<template #content>
|
||||
<div class="p-6" style="min-width: 480px; max-height: 80vh; overflow-y: auto">
|
||||
<h3 class="mb-4 text-lg font-semibold text-gray-900 dark:text-white">
|
||||
+1
-1
@@ -48,7 +48,7 @@ function save() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UModal :open="open" @update:open="emit('update:open', $event)">
|
||||
<UModal :open="open" :ui="{ content: 'z-[101]', overlay: 'z-[100]' }" @update:open="emit('update:open', $event)">
|
||||
<template #content>
|
||||
<div class="p-6" style="min-width: 420px">
|
||||
<h3 class="mb-4 text-lg font-semibold text-gray-900 dark:text-white">
|
||||
+1
-1
@@ -468,7 +468,7 @@ function subscribedRemoteIds(ds: DataSource): Set<string> {
|
||||
/>
|
||||
|
||||
<!-- Delete confirmation modal -->
|
||||
<UModal v-model:open="showDeleteModal">
|
||||
<UModal v-model:open="showDeleteModal" :ui="{ content: 'z-[101]', overlay: 'z-[100]' }">
|
||||
<template #content>
|
||||
<div class="p-4">
|
||||
<h3 class="mb-3 font-semibold text-gray-900 dark:text-white">
|
||||
+49
-23
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed } from 'vue'
|
||||
import { computed, ref, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useLayoutStore } from '@/stores/layout'
|
||||
import { useSettingsStore } from '@/stores/settings'
|
||||
@@ -17,6 +17,27 @@ const settingsStore = useSettingsStore()
|
||||
const { screenshotMobileAdapt } = storeToRefs(layoutStore)
|
||||
const { locale, defaultSessionTab } = storeToRefs(settingsStore)
|
||||
|
||||
// Auto Launch
|
||||
const openAtLogin = ref(false)
|
||||
const isPackaged = ref(true)
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const enabled = await window.api.app.getOpenAtLogin()
|
||||
openAtLogin.value = enabled
|
||||
} catch {
|
||||
isPackaged.value = false
|
||||
}
|
||||
})
|
||||
|
||||
async function handleAutoLaunchChange(enabled: boolean) {
|
||||
const { success } = await window.api.app.setOpenAtLogin(enabled)
|
||||
if (!success) {
|
||||
openAtLogin.value = !enabled
|
||||
isPackaged.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Color Mode
|
||||
const colorMode = useColorMode({
|
||||
emitAuto: true,
|
||||
@@ -63,15 +84,15 @@ watch(
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<!-- 语言设置 -->
|
||||
<div class="space-y-6 pb-6">
|
||||
<!-- 常规:语言 + 开机自启动 -->
|
||||
<div>
|
||||
<h3 class="mb-3 flex items-center gap-2 text-sm font-semibold text-gray-900 dark:text-white">
|
||||
<UIcon name="i-heroicons-language" class="h-4 w-4 text-green-500" />
|
||||
{{ t('settings.basic.language.title') }}
|
||||
<UIcon name="i-heroicons-cog-6-tooth" class="h-4 w-4 text-gray-500" />
|
||||
{{ t('settings.basic.general.title') }}
|
||||
</h3>
|
||||
<div class="rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-800/50">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="rounded-lg border border-gray-200 bg-gray-50 dark:border-gray-700 dark:bg-gray-800/50">
|
||||
<div class="flex items-center justify-between p-4">
|
||||
<div class="flex-1 pr-4">
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{{ t('settings.basic.language.description') }}
|
||||
@@ -81,6 +102,20 @@ watch(
|
||||
<UITabs v-model="currentLocale" size="sm" class="gap-0" :items="languageOptions"></UITabs>
|
||||
</div>
|
||||
</div>
|
||||
<div class="border-t border-gray-200 dark:border-gray-700"></div>
|
||||
<div class="flex items-center justify-between p-4">
|
||||
<div class="flex-1 pr-4">
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{{ t('settings.basic.autoLaunch.openAtLogin') }}
|
||||
</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{
|
||||
isPackaged ? t('settings.basic.autoLaunch.openAtLoginDesc') : t('settings.basic.autoLaunch.devModeHint')
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
<USwitch v-model="openAtLogin" :disabled="!isPackaged" @update:model-value="handleAutoLaunchChange" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -104,14 +139,14 @@ watch(
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 默认标签页 -->
|
||||
<!-- 偏好设置:默认标签页 + 截图 -->
|
||||
<div>
|
||||
<h3 class="mb-3 flex items-center gap-2 text-sm font-semibold text-gray-900 dark:text-white">
|
||||
<UIcon name="i-heroicons-rectangle-stack" class="h-4 w-4 text-purple-500" />
|
||||
{{ t('settings.basic.defaultTab.title') }}
|
||||
<UIcon name="i-heroicons-adjustments-horizontal" class="h-4 w-4 text-purple-500" />
|
||||
{{ t('settings.basic.preferences.title') }}
|
||||
</h3>
|
||||
<div class="rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-800/50">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="rounded-lg border border-gray-200 bg-gray-50 dark:border-gray-700 dark:bg-gray-800/50">
|
||||
<div class="flex items-center justify-between p-4">
|
||||
<div class="flex-1 pr-4">
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{{ t('settings.basic.defaultTab.description') }}
|
||||
@@ -121,17 +156,8 @@ watch(
|
||||
<UTabs v-model="defaultSessionTab" size="sm" class="gap-0" :items="defaultTabOptions"></UTabs>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 截图设置 -->
|
||||
<div>
|
||||
<h3 class="mb-3 flex items-center gap-2 text-sm font-semibold text-gray-900 dark:text-white">
|
||||
<UIcon name="i-heroicons-camera" class="h-4 w-4 text-blue-500" />
|
||||
{{ t('settings.basic.screenshot.title') }}
|
||||
</h3>
|
||||
<div class="rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-800/50">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="border-t border-gray-200 dark:border-gray-700"></div>
|
||||
<div class="flex items-center justify-between p-4">
|
||||
<div class="flex-1 pr-4">
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{{ t('settings.basic.screenshot.mobileAdapt') }}
|
||||
+2
-2
@@ -710,7 +710,7 @@ onMounted(() => {
|
||||
</div>
|
||||
|
||||
<!-- 合并确认弹窗 -->
|
||||
<UModal v-model:open="showMergeModal" :ui="{ content: 'z-100' }">
|
||||
<UModal v-model:open="showMergeModal" :ui="{ content: 'z-[101]', overlay: 'z-[100]' }">
|
||||
<template #content>
|
||||
<div class="p-4">
|
||||
<div class="mb-4 flex items-center gap-3">
|
||||
@@ -781,7 +781,7 @@ onMounted(() => {
|
||||
</UModal>
|
||||
|
||||
<!-- 删除确认弹窗 -->
|
||||
<UModal v-model:open="showDeleteModal">
|
||||
<UModal v-model:open="showDeleteModal" :ui="{ content: 'z-[101]', overlay: 'z-[100]' }">
|
||||
<template #content>
|
||||
<div class="p-4">
|
||||
<div class="mb-4 flex items-center gap-3">
|
||||
@@ -2,4 +2,3 @@ export { default as AISettingsTab } from './AISettingsTab.vue'
|
||||
export { default as AIModelConfigTab } from './AI/AIModelConfigTab.vue'
|
||||
export { default as AIModelEditModal } from './AI/AIModelEditModal.vue'
|
||||
export { default as AIPromptConfigTab } from './AI/AIPromptConfigTab.vue'
|
||||
export { default as CacheManageTab } from './CacheManageTab.vue'
|
||||
@@ -0,0 +1,162 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, computed, nextTick } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import AISettingsTab from './Settings/AISettingsTab.vue'
|
||||
import BasicSettingsTab from './Settings/BasicSettingsTab.vue'
|
||||
import BatchManageTab from './Settings/BatchManageTab.vue'
|
||||
import StorageTab from './Settings/StorageTab.vue'
|
||||
import AboutTab from './Settings/AboutTab.vue'
|
||||
import ApiSettingsTab from './Settings/ApiSettingsTab.vue'
|
||||
import { usePromptStore } from '@/stores/prompt'
|
||||
import { useLayoutStore } from '@/stores/layout'
|
||||
|
||||
const { t } = useI18n()
|
||||
const promptStore = usePromptStore()
|
||||
const layoutStore = useLayoutStore()
|
||||
const { showSettings, settingsTab, settingsSubTab } = storeToRefs(layoutStore)
|
||||
|
||||
interface ScrollableTab {
|
||||
scrollToSection?: (sectionId: string) => void
|
||||
refresh?: () => void
|
||||
}
|
||||
|
||||
const tabs = computed(() => [
|
||||
{ id: 'settings', label: t('settings.tabs.basic'), icon: 'i-heroicons-cog-6-tooth' },
|
||||
{ id: 'ai', label: t('settings.tabs.ai'), icon: 'i-heroicons-sparkles' },
|
||||
{ id: 'data', label: t('settings.tabs.dataManage'), icon: 'i-heroicons-rectangle-stack' },
|
||||
{ id: 'api', label: t('settings.tabs.api'), icon: 'i-heroicons-server-stack' },
|
||||
{ id: 'storage', label: t('settings.tabs.storage'), icon: 'i-heroicons-folder-open' },
|
||||
{ id: 'about', label: t('settings.tabs.about'), icon: 'i-heroicons-information-circle' },
|
||||
])
|
||||
|
||||
const activeTab = ref('settings')
|
||||
|
||||
const tabRefs = ref<Record<string, ScrollableTab | null>>({})
|
||||
|
||||
function setTabRef(tabId: string, el: unknown) {
|
||||
tabRefs.value[tabId] = el as ScrollableTab | null
|
||||
}
|
||||
|
||||
function handleAIConfigChanged() {
|
||||
promptStore.notifyAIConfigChanged()
|
||||
}
|
||||
|
||||
function switchTab(tabId: string) {
|
||||
activeTab.value = tabId
|
||||
nextTick(() => {
|
||||
tabRefs.value[tabId]?.refresh?.()
|
||||
})
|
||||
}
|
||||
|
||||
function scrollToSubTab(subTab: string) {
|
||||
const tabRef = tabRefs.value[activeTab.value]
|
||||
if (tabRef?.scrollToSection) {
|
||||
tabRef.scrollToSection(subTab)
|
||||
}
|
||||
}
|
||||
|
||||
watch(showSettings, async (visible) => {
|
||||
if (visible) {
|
||||
activeTab.value = settingsTab.value || 'settings'
|
||||
if (settingsSubTab.value) {
|
||||
await nextTick()
|
||||
setTimeout(() => scrollToSubTab(settingsSubTab.value!), 100)
|
||||
}
|
||||
nextTick(() => {
|
||||
tabRefs.value[activeTab.value]?.refresh?.()
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UModal v-model:open="showSettings" :ui="{ content: 'sm:max-w-[900px] z-[100]', overlay: 'backdrop-blur-sm z-[99]' }">
|
||||
<template #content>
|
||||
<div class="flex min-h-[650px] h-[85vh] flex-col overflow-hidden">
|
||||
<!-- Header -->
|
||||
<div class="shrink-0 border-b border-gray-200 px-6 pt-5 pb-0 dark:border-gray-800">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex h-8 w-8 items-center justify-center rounded-lg bg-primary-600 dark:bg-primary-500">
|
||||
<UIcon name="i-heroicons-cog-6-tooth" class="h-4 w-4 text-white" />
|
||||
</div>
|
||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{{ t('settings.title') }}
|
||||
</h2>
|
||||
</div>
|
||||
<UButton
|
||||
icon="i-heroicons-x-mark"
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@click="layoutStore.closeSettings()"
|
||||
/>
|
||||
</div>
|
||||
<!-- Tabs -->
|
||||
<div class="mt-4 flex items-center gap-1 overflow-x-auto pb-3 scrollbar-hide">
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
:key="tab.id"
|
||||
class="flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium transition-all"
|
||||
:class="[
|
||||
activeTab === tab.id
|
||||
? 'bg-pink-500 text-white dark:bg-pink-900/30 dark:text-pink-300'
|
||||
: 'text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800',
|
||||
]"
|
||||
@click="switchTab(tab.id)"
|
||||
>
|
||||
<UIcon :name="tab.icon" class="h-4 w-4" />
|
||||
<span class="whitespace-nowrap">{{ tab.label }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="relative flex-1">
|
||||
<div class="absolute inset-0 p-6">
|
||||
<Transition name="tab-slide" mode="out-in">
|
||||
<div v-if="activeTab === 'settings'" key="settings" class="h-full overflow-y-auto">
|
||||
<BasicSettingsTab />
|
||||
</div>
|
||||
<AISettingsTab
|
||||
v-else-if="activeTab === 'ai'"
|
||||
key="ai"
|
||||
:ref="(el: unknown) => setTabRef('ai', el)"
|
||||
@config-changed="handleAIConfigChanged"
|
||||
/>
|
||||
<BatchManageTab v-else-if="activeTab === 'data'" key="data" />
|
||||
<ApiSettingsTab v-else-if="activeTab === 'api'" key="api" />
|
||||
<StorageTab
|
||||
v-else-if="activeTab === 'storage'"
|
||||
key="storage"
|
||||
:ref="(el: unknown) => setTabRef('storage', el)"
|
||||
/>
|
||||
<div v-else-if="activeTab === 'about'" key="about" class="h-full overflow-y-auto">
|
||||
<AboutTab />
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</UModal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.tab-slide-enter-active,
|
||||
.tab-slide-leave-active {
|
||||
transition:
|
||||
opacity 0.2s ease,
|
||||
transform 0.2s ease;
|
||||
}
|
||||
|
||||
.tab-slide-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
|
||||
.tab-slide-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
</style>
|
||||
@@ -259,7 +259,7 @@ function getSessionAvatar(session: AnalysisSession): string | null {
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
@click="router.push({ name: 'settings', query: { tab: 'data' } })"
|
||||
@click="layoutStore.openSettings('data')"
|
||||
/>
|
||||
</UTooltip>
|
||||
</div>
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useLayoutStore } from '@/stores/layout'
|
||||
import SidebarButton from './SidebarButton.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const isSettingsPage = computed(() => route.name === 'settings')
|
||||
const layoutStore = useLayoutStore()
|
||||
const { showSettings } = storeToRefs(layoutStore)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -17,8 +15,8 @@ const isSettingsPage = computed(() => route.name === 'settings')
|
||||
<SidebarButton
|
||||
icon="i-heroicons-cog-6-tooth"
|
||||
:title="t('layout.footer.settings')"
|
||||
:active="isSettingsPage"
|
||||
@click="router.push({ name: 'settings' })"
|
||||
:active="showSettings"
|
||||
@click="layoutStore.openSettings()"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -4,35 +4,61 @@
|
||||
* 包含标题、描述、可选头像/图标,以及默认 slot 用于额外内容
|
||||
*/
|
||||
|
||||
defineProps<{
|
||||
title: string
|
||||
description?: string
|
||||
icon?: string // fallback 图标
|
||||
iconClass?: string // 图标背景样式类
|
||||
avatar?: string | null // 头像图片(base64 Data URL),优先级高于 icon
|
||||
}>()
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
title: string
|
||||
description?: string
|
||||
icon?: string // fallback 图标
|
||||
iconClass?: string // 图标背景样式类
|
||||
avatar?: string | null // 头像图片(base64 Data URL),优先级高于 icon
|
||||
size?: 'default' | 'compact' // 紧凑模式用于需要更小头部高度的页面
|
||||
}>(),
|
||||
{
|
||||
size: 'default',
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative border-b border-gray-200/50 px-6 pb-2 dark:border-gray-800/50">
|
||||
<div
|
||||
class="relative border-b border-gray-200/50 dark:border-gray-800/50"
|
||||
:class="props.size === 'compact' ? 'px-5 pb-1.5' : 'px-6 pb-2'"
|
||||
>
|
||||
<!-- 拖拽区域 - 覆盖顶部安全区域(平台自适应)
|
||||
macOS: 16px padding + 16px = 32px | Windows/Linux: 32px padding + 16px = 48px -->
|
||||
<div class="titlebar-drag-cover" />
|
||||
|
||||
<!-- 标题区域 -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex items-center" :class="props.size === 'compact' ? 'gap-2.5' : 'gap-3'">
|
||||
<!-- 头像图片(优先显示) -->
|
||||
<img v-if="avatar" :src="avatar" :alt="title" class="h-10 w-10 rounded-xl object-cover" />
|
||||
<img
|
||||
v-if="avatar"
|
||||
:src="avatar"
|
||||
:alt="title"
|
||||
class="object-cover"
|
||||
:class="props.size === 'compact' ? 'h-8 w-8 rounded-lg' : 'h-10 w-10 rounded-xl'"
|
||||
/>
|
||||
<!-- 可选图标(fallback) -->
|
||||
<div v-else-if="icon" class="flex h-10 w-10 items-center justify-center rounded-xl" :class="iconClass">
|
||||
<UIcon :name="icon" class="h-5 w-5 text-white" />
|
||||
<div
|
||||
v-else-if="icon"
|
||||
class="flex items-center justify-center"
|
||||
:class="[iconClass, props.size === 'compact' ? 'h-8 w-8 rounded-lg' : 'h-10 w-10 rounded-xl']"
|
||||
>
|
||||
<UIcon :name="icon" class="text-white" :class="props.size === 'compact' ? 'h-4 w-4' : 'h-5 w-5'" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
<h1
|
||||
class="font-semibold text-gray-900 dark:text-white"
|
||||
:class="props.size === 'compact' ? 'text-base' : 'text-lg'"
|
||||
>
|
||||
{{ title }}
|
||||
</h1>
|
||||
<p v-if="description" class="text-xs text-gray-500 dark:text-gray-400">
|
||||
<p
|
||||
v-if="description"
|
||||
class="text-gray-500 dark:text-gray-400"
|
||||
:class="props.size === 'compact' ? 'text-[11px]' : 'text-xs'"
|
||||
>
|
||||
{{ description }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -32,6 +32,8 @@
|
||||
"previewUnavailable": "No skill prompt available for preview",
|
||||
"presetMore": "More",
|
||||
"presetCollapse": "Collapse",
|
||||
"quickAsk": "Quick Ask",
|
||||
"editQuickAsk": "Edit Quick Ask",
|
||||
"send": "Send",
|
||||
"needMoreThanSkill": "Selecting a skill alone is not enough. Enter a specific question.",
|
||||
"needQuestion": "Enter a question before sending"
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
"deepseek": {
|
||||
"name": "DeepSeek",
|
||||
"models": {
|
||||
"deepseek-chat": "General chat model",
|
||||
"deepseek-coder": "Code generation model"
|
||||
"deepseek-v4-pro": "Flagship model, 1M context, thinking & non-thinking",
|
||||
"deepseek-v4-flash": "Fast model, 1M context, thinking & non-thinking"
|
||||
}
|
||||
},
|
||||
"qwen": {
|
||||
|
||||
@@ -19,6 +19,12 @@
|
||||
}
|
||||
},
|
||||
"basic": {
|
||||
"general": {
|
||||
"title": "General"
|
||||
},
|
||||
"preferences": {
|
||||
"title": "Preferences"
|
||||
},
|
||||
"language": {
|
||||
"title": "Language",
|
||||
"description": "Choose language"
|
||||
@@ -32,10 +38,16 @@
|
||||
},
|
||||
"defaultTab": {
|
||||
"title": "Default Tab",
|
||||
"description": "Default page when entering a session",
|
||||
"description": "Default page when entering a chat conversation",
|
||||
"overview": "Overview",
|
||||
"aiChat": "AI Chat"
|
||||
},
|
||||
"autoLaunch": {
|
||||
"title": "Startup",
|
||||
"openAtLogin": "Launch at Login",
|
||||
"openAtLoginDesc": "Automatically start ChatLab when the system boots",
|
||||
"devModeHint": "Not available in dev mode"
|
||||
},
|
||||
"screenshot": {
|
||||
"title": "Screenshot",
|
||||
"mobileAdapt": "Mobile Adaptation",
|
||||
|
||||
@@ -32,6 +32,8 @@
|
||||
"previewUnavailable": "プレビューできるスキルプロンプトがありません",
|
||||
"presetMore": "もっと見る",
|
||||
"presetCollapse": "折りたたむ",
|
||||
"quickAsk": "クイック質問",
|
||||
"editQuickAsk": "クイック質問を編集",
|
||||
"send": "送信",
|
||||
"needMoreThanSkill": "スキルを選んだだけでは送信できません。具体的な質問を入力してください。",
|
||||
"needQuestion": "質問を入力してから送信してください"
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
"deepseek": {
|
||||
"name": "DeepSeek",
|
||||
"models": {
|
||||
"deepseek-chat": "汎用チャットモデル",
|
||||
"deepseek-coder": "コード生成モデル"
|
||||
"deepseek-v4-pro": "フラッグシップモデル、1M コンテキスト、思考・非思考モード対応",
|
||||
"deepseek-v4-flash": "高速モデル、1M コンテキスト、思考・非思考モード対応"
|
||||
}
|
||||
},
|
||||
"qwen": {
|
||||
|
||||
@@ -19,6 +19,12 @@
|
||||
}
|
||||
},
|
||||
"basic": {
|
||||
"general": {
|
||||
"title": "一般"
|
||||
},
|
||||
"preferences": {
|
||||
"title": "環境設定"
|
||||
},
|
||||
"language": {
|
||||
"title": "言語",
|
||||
"description": "アプリの表示言語を選択します"
|
||||
@@ -32,10 +38,16 @@
|
||||
},
|
||||
"defaultTab": {
|
||||
"title": "デフォルトタブ",
|
||||
"description": "セッションに入った時に表示するページ",
|
||||
"description": "チャットに入った時に表示するページ",
|
||||
"overview": "概要",
|
||||
"aiChat": "AI チャット"
|
||||
},
|
||||
"autoLaunch": {
|
||||
"title": "起動設定",
|
||||
"openAtLogin": "ログイン時に自動起動",
|
||||
"openAtLoginDesc": "システム起動時に ChatLab を自動的に実行します",
|
||||
"devModeHint": "開発モードでは利用できません"
|
||||
},
|
||||
"screenshot": {
|
||||
"title": "スクリーンショット設定",
|
||||
"mobileAdapt": "モバイル対応",
|
||||
|
||||
@@ -32,6 +32,8 @@
|
||||
"previewUnavailable": "暂无可预览的技能提示词",
|
||||
"presetMore": "更多",
|
||||
"presetCollapse": "收起",
|
||||
"quickAsk": "快速提问",
|
||||
"editQuickAsk": "编辑快速提问",
|
||||
"send": "发送",
|
||||
"needMoreThanSkill": "仅选择技能还不能发送,请继续输入具体问题",
|
||||
"needQuestion": "请输入问题后再发送"
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
"deepseek": {
|
||||
"name": "DeepSeek",
|
||||
"models": {
|
||||
"deepseek-chat": "通用对话模型",
|
||||
"deepseek-coder": "代码生成模型"
|
||||
"deepseek-v4-pro": "旗舰模型,1M 上下文,支持思考与非思考模式",
|
||||
"deepseek-v4-flash": "高速模型,1M 上下文,支持思考与非思考模式"
|
||||
}
|
||||
},
|
||||
"qwen": {
|
||||
|
||||
@@ -19,6 +19,12 @@
|
||||
}
|
||||
},
|
||||
"basic": {
|
||||
"general": {
|
||||
"title": "常规"
|
||||
},
|
||||
"preferences": {
|
||||
"title": "偏好设置"
|
||||
},
|
||||
"language": {
|
||||
"title": "语言",
|
||||
"description": "选择软件显示语言"
|
||||
@@ -32,10 +38,16 @@
|
||||
},
|
||||
"defaultTab": {
|
||||
"title": "默认标签页",
|
||||
"description": "进入会话后默认显示的页面",
|
||||
"description": "进入聊天对话后默认显示的页面",
|
||||
"overview": "总览",
|
||||
"aiChat": "AI 对话"
|
||||
},
|
||||
"autoLaunch": {
|
||||
"title": "启动设置",
|
||||
"openAtLogin": "开机自启动",
|
||||
"openAtLoginDesc": "系统启动时自动运行 ChatLab",
|
||||
"devModeHint": "开发模式下不可用"
|
||||
},
|
||||
"screenshot": {
|
||||
"title": "截图设置",
|
||||
"mobileAdapt": "移动端适配",
|
||||
|
||||
@@ -32,6 +32,8 @@
|
||||
"previewUnavailable": "暫無可預覽的技能提示詞",
|
||||
"presetMore": "更多",
|
||||
"presetCollapse": "收起",
|
||||
"quickAsk": "快速提問",
|
||||
"editQuickAsk": "編輯快速提問",
|
||||
"send": "送出",
|
||||
"needMoreThanSkill": "只選擇技能還不能送出,請繼續輸入具體問題",
|
||||
"needQuestion": "請輸入問題後再送出"
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
"deepseek": {
|
||||
"name": "DeepSeek",
|
||||
"models": {
|
||||
"deepseek-chat": "通用對話模型",
|
||||
"deepseek-coder": "程式碼生成模型"
|
||||
"deepseek-v4-pro": "旗艦模型,1M 上下文,支援思考與非思考模式",
|
||||
"deepseek-v4-flash": "高速模型,1M 上下文,支援思考與非思考模式"
|
||||
}
|
||||
},
|
||||
"qwen": {
|
||||
|
||||
@@ -19,6 +19,12 @@
|
||||
}
|
||||
},
|
||||
"basic": {
|
||||
"general": {
|
||||
"title": "一般"
|
||||
},
|
||||
"preferences": {
|
||||
"title": "偏好設定"
|
||||
},
|
||||
"language": {
|
||||
"title": "語言",
|
||||
"description": "選擇軟體顯示語言"
|
||||
@@ -32,10 +38,16 @@
|
||||
},
|
||||
"defaultTab": {
|
||||
"title": "預設標籤頁",
|
||||
"description": "進入會話後預設顯示的頁面",
|
||||
"description": "進入聊天對話後預設顯示的頁面",
|
||||
"overview": "總覽",
|
||||
"aiChat": "AI 對話"
|
||||
},
|
||||
"autoLaunch": {
|
||||
"title": "啟動設定",
|
||||
"openAtLogin": "開機自動啟動",
|
||||
"openAtLoginDesc": "系統啟動時自動執行 ChatLab",
|
||||
"devModeHint": "開發模式下不可用"
|
||||
},
|
||||
"screenshot": {
|
||||
"title": "截圖設定",
|
||||
"mobileAdapt": "行動畫面適配",
|
||||
|
||||
@@ -131,7 +131,7 @@ const { headerDescription } = useSessionHeaderDescription({
|
||||
<CaptureButton />
|
||||
</template>
|
||||
<!-- Tabs -->
|
||||
<div class="mt-4 flex items-center justify-between gap-3">
|
||||
<div class="mt-2 flex items-center justify-between gap-3">
|
||||
<div class="flex shrink-0 items-center gap-0.5 overflow-x-auto scrollbar-hide">
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
|
||||
@@ -43,7 +43,7 @@ function getDefaultLinks(): FooterLink[] {
|
||||
id: 'github',
|
||||
icon: 'i-simple-icons-github',
|
||||
title: 'Github',
|
||||
url: 'https://github.com/hellodigua/ChatLab',
|
||||
url: 'https://github.com/ChatLab/ChatLab',
|
||||
},
|
||||
{
|
||||
id: 'terms',
|
||||
|
||||
@@ -57,13 +57,13 @@ const features = computed(() => [
|
||||
|
||||
<template>
|
||||
<div class="relative flex h-full w-full overflow-hidden pt-4">
|
||||
<!-- 顶部窗口拖拽区域,固定 50px,覆盖应用最上方 -->
|
||||
<div class="absolute left-0 right-0 top-0 z-10 h-[50px]" style="-webkit-app-region: drag" />
|
||||
<!-- Content Container -->
|
||||
<div class="relative h-full w-full overflow-y-auto">
|
||||
<div class="flex min-h-full w-full flex-col items-center justify-center px-4 py-12">
|
||||
<!-- Hero Section -->
|
||||
<div class="relative xl:mb-6 mb-4 w-full text-center">
|
||||
<!-- 标题上方可拖拽区域,向上扩展覆盖空隙 -->
|
||||
<div class="absolute -top-32 left-0 right-0 h-32" style="-webkit-app-region: drag" />
|
||||
<!-- Title -->
|
||||
<h1 class="mb-4 select-none text-5xl sm:text-5xl lg:text-6xl font-black tracking-tight text-pink-500">
|
||||
{{ t('home.title') }}
|
||||
|
||||
@@ -136,6 +136,7 @@ const otherMemberAvatar = computed(() => {
|
||||
:title="session.name"
|
||||
:description="headerDescription"
|
||||
:avatar="otherMemberAvatar"
|
||||
size="compact"
|
||||
icon="i-heroicons-user"
|
||||
icon-class="bg-pink-600 text-white dark:bg-pink-500 dark:text-white"
|
||||
>
|
||||
@@ -143,7 +144,7 @@ const otherMemberAvatar = computed(() => {
|
||||
<CaptureButton />
|
||||
</template>
|
||||
<!-- Tabs -->
|
||||
<div class="mt-4 flex items-center justify-between gap-3">
|
||||
<div class="mt-3 flex items-center justify-between gap-3">
|
||||
<div class="flex shrink-0 items-center gap-0.5 overflow-x-auto scrollbar-hide">
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
|
||||
@@ -3,12 +3,12 @@ import { ref, watch, onMounted, computed, nextTick } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import PageHeader from '@/components/layout/PageHeader.vue'
|
||||
import AISettingsTab from './components/AISettingsTab.vue'
|
||||
import BasicSettingsTab from './components/BasicSettingsTab.vue'
|
||||
import BatchManageTab from './components/BatchManageTab.vue'
|
||||
import StorageTab from './components/StorageTab.vue'
|
||||
import AboutTab from './components/AboutTab.vue'
|
||||
import ApiSettingsTab from './components/ApiSettingsTab.vue'
|
||||
import AISettingsTab from '@/components/common/Settings/AISettingsTab.vue'
|
||||
import BasicSettingsTab from '@/components/common/Settings/BasicSettingsTab.vue'
|
||||
import BatchManageTab from '@/components/common/Settings/BatchManageTab.vue'
|
||||
import StorageTab from '@/components/common/Settings/StorageTab.vue'
|
||||
import AboutTab from '@/components/common/Settings/AboutTab.vue'
|
||||
import ApiSettingsTab from '@/components/common/Settings/ApiSettingsTab.vue'
|
||||
import { usePromptStore } from '@/stores/prompt'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -17,11 +17,6 @@ export const router = createRouter({
|
||||
name: 'private-chat',
|
||||
component: () => import('@/pages/private-chat/index.vue'),
|
||||
},
|
||||
{
|
||||
path: '/settings',
|
||||
name: 'settings',
|
||||
component: () => import('@/pages/settings/index.vue'),
|
||||
},
|
||||
],
|
||||
history: createWebHashHistory(),
|
||||
})
|
||||
|
||||
@@ -20,6 +20,11 @@ export const useLayoutStore = defineStore(
|
||||
// 截图设置
|
||||
const screenshotMobileAdapt = ref(false) // 截图时开启移动端适配,默认关闭
|
||||
|
||||
// 设置弹窗
|
||||
const showSettings = ref(false)
|
||||
const settingsTab = ref<string>('settings')
|
||||
const settingsSubTab = ref<string | null>(null)
|
||||
|
||||
/**
|
||||
* 切换侧边栏展开/折叠状态
|
||||
*/
|
||||
@@ -67,6 +72,19 @@ export const useLayoutStore = defineStore(
|
||||
isToolsPanelLocked.value = !isToolsPanelLocked.value
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开设置弹窗,可选指定 Tab 和 SubTab
|
||||
*/
|
||||
function openSettings(tab?: string, subTab?: string) {
|
||||
settingsTab.value = tab || 'settings'
|
||||
settingsSubTab.value = subTab || null
|
||||
showSettings.value = true
|
||||
}
|
||||
|
||||
function closeSettings() {
|
||||
showSettings.value = false
|
||||
}
|
||||
|
||||
function toggleToolsPanelMini() {
|
||||
isToolsPanelMini.value = !isToolsPanelMini.value
|
||||
if (isToolsPanelMini.value) {
|
||||
@@ -83,6 +101,9 @@ export const useLayoutStore = defineStore(
|
||||
showChatRecordDrawer,
|
||||
chatRecordQuery,
|
||||
screenshotMobileAdapt,
|
||||
showSettings,
|
||||
settingsTab,
|
||||
settingsSubTab,
|
||||
toggleSidebar,
|
||||
toggleToolsPanelLock,
|
||||
toggleToolsPanelMini,
|
||||
@@ -90,6 +111,8 @@ export const useLayoutStore = defineStore(
|
||||
closeScreenCaptureModal,
|
||||
openChatRecordDrawer,
|
||||
closeChatRecordDrawer,
|
||||
openSettings,
|
||||
closeSettings,
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user