@@ -36,9 +50,15 @@
@@ -51,6 +71,8 @@
import { ref, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
+import UiIcon from '@/components/UiIcon.vue'
+import ThemeToggle from '@/components/ThemeToggle.vue'
const route = useRoute()
const router = useRouter()
@@ -69,7 +91,7 @@ const navGroups = computed(() => {
group.items.push({
path: menu.path,
label: menu.name,
- icon: menu.icon || '📄'
+ icon: menu.icon || 'file'
})
}
return groups
@@ -93,161 +115,284 @@ function handleLogout() {
.admin-layout {
display: flex;
height: 100%;
+ min-height: 100dvh;
}
.sidebar {
- width: var(--sidebar-width);
- background: var(--bg-secondary);
- border-right: 1px solid var(--border);
+ position: relative;
display: flex;
+ width: var(--sidebar-width);
+ flex: 0 0 var(--sidebar-width);
flex-direction: column;
- flex-shrink: 0;
+ overflow: hidden;
+ border-right: 1px solid var(--border);
+ background:
+ radial-gradient(circle at 45% -10%, rgba(183, 243, 107, 0.08), transparent 25%),
+ rgba(13, 16, 21, 0.96);
+ box-shadow: 18px 0 50px rgba(0, 0, 0, 0.16);
+ backdrop-filter: blur(20px);
+}
+
+.sidebar::after {
+ position: absolute;
+ top: 0;
+ right: -1px;
+ width: 1px;
+ height: 24%;
+ background: linear-gradient(to bottom, var(--accent), transparent);
+ box-shadow: 0 0 16px rgba(183, 243, 107, 0.42);
+ content: "";
}
.sidebar-brand {
display: flex;
align-items: center;
- gap: 10px;
- padding: 20px 16px;
- font-size: 16px;
- font-weight: 600;
- border-bottom: 1px solid var(--border);
+ gap: 11px;
+ padding: 20px 18px 18px;
}
.brand-icon {
- font-size: 20px;
+ display: grid;
+ width: 36px;
+ height: 36px;
+ flex: 0 0 auto;
+ place-items: center;
+ border: 1px solid rgba(183, 243, 107, 0.42);
+ border-radius: 11px;
+ background: var(--accent);
+ color: #12170f;
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.5), 0 4px 0 #55782f, 0 10px 24px rgba(110, 166, 54, 0.18);
+}
+
+.brand-copy {
+ display: flex;
+ min-width: 0;
+ flex-direction: column;
+ gap: 2px;
+}
+
+.brand-copy strong {
+ font-size: 16px;
+ letter-spacing: -0.02em;
+}
+
+.brand-copy small {
+ color: var(--text-muted);
+ font-family: "Cascadia Code", Consolas, monospace;
+ font-size: 9px;
+ letter-spacing: 0.14em;
}
.sidebar-member-link {
- display: block;
- margin: 0 12px 8px;
- padding: 8px 12px;
- font-size: 13px;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin: 0 12px 10px;
+ padding: 9px 11px;
+ border: 1px solid var(--border);
+ border-radius: 10px;
+ background: rgba(255, 255, 255, 0.02);
color: var(--text-secondary);
- text-decoration: none;
- border-radius: 8px;
+ font-size: 12px;
+ transition: border-color 160ms ease, background 160ms ease, color 160ms ease, transform 160ms ease;
}
+
.sidebar-member-link:hover {
+ border-color: rgba(183, 243, 107, 0.24);
+ background: var(--accent-soft);
color: var(--accent);
- background: var(--bg-hover);
+ transform: translateX(2px);
}
.sidebar-nav {
flex: 1;
- padding: 12px 8px;
+ padding: 4px 10px 14px;
overflow-y: auto;
}
.nav-group-title {
- padding: 12px 12px 6px;
- font-size: 11px;
- font-weight: 600;
+ padding: 15px 10px 7px;
color: var(--text-muted);
- letter-spacing: 0.04em;
+ font-size: 10px;
+ font-weight: 750;
+ letter-spacing: 0.11em;
text-transform: uppercase;
}
.nav-item {
+ position: relative;
display: flex;
+ min-height: 42px;
align-items: center;
gap: 10px;
- padding: 10px 12px;
- border-radius: 8px;
+ margin-bottom: 3px;
+ padding: 8px 10px;
+ overflow: hidden;
+ border: 1px solid transparent;
+ border-radius: 11px;
color: var(--text-secondary);
- font-size: 14px;
- margin-bottom: 2px;
- transition: all 0.15s;
+ font-size: 13px;
+ transition: transform 180ms var(--ease-spring), border-color 160ms ease, background 160ms ease, color 160ms ease;
}
.nav-item:hover {
- background: var(--bg-hover);
+ border-color: var(--border);
+ background: rgba(255, 255, 255, 0.035);
color: var(--text-primary);
+ transform: translateX(2px);
}
.nav-item.router-link-active {
- background: var(--accent);
- color: white;
+ border-color: rgba(183, 243, 107, 0.28);
+ background: linear-gradient(90deg, rgba(183, 243, 107, 0.14), rgba(183, 243, 107, 0.045));
+ color: var(--accent);
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.025);
}
.nav-icon {
- font-size: 16px;
+ display: grid;
+ width: 28px;
+ height: 28px;
+ flex: 0 0 auto;
+ place-items: center;
+ border-radius: 8px;
+ background: rgba(255, 255, 255, 0.035);
+}
+
+.router-link-active .nav-icon {
+ background: rgba(183, 243, 107, 0.12);
+ box-shadow: 0 0 18px rgba(183, 243, 107, 0.08);
+}
+
+.nav-label {
+ flex: 1;
+}
+
+.nav-pip {
+ width: 5px;
+ height: 5px;
+ border-radius: 50%;
+ background: currentColor;
+ opacity: 0;
+ box-shadow: 0 0 9px currentColor;
+ transition: opacity 160ms ease;
+}
+
+.router-link-active .nav-pip {
+ opacity: 1;
}
.sidebar-footer {
- padding: 12px;
+ padding: 13px;
border-top: 1px solid var(--border);
+ background: rgba(0, 0, 0, 0.14);
+}
+
+.sidebar-theme-toggle {
+ margin: 0 4px 8px;
}
.admin-user {
display: flex;
align-items: center;
- gap: 8px;
- padding: 8px;
- font-size: 13px;
- margin-bottom: 8px;
+ gap: 10px;
+ margin-bottom: 10px;
+ padding: 6px 4px;
}
.avatar {
- width: 28px;
- height: 28px;
- border-radius: 50%;
- background: var(--accent);
- display: flex;
- align-items: center;
- justify-content: center;
+ display: grid;
+ width: 34px;
+ height: 34px;
+ flex: 0 0 auto;
+ place-items: center;
+ border: 1px solid rgba(183, 243, 107, 0.3);
+ border-radius: 10px;
+ background: var(--accent-soft);
+ color: var(--accent);
font-size: 12px;
- font-weight: 600;
+ font-weight: 750;
+}
+
+.admin-user-copy {
+ display: flex;
+ min-width: 0;
+ flex-direction: column;
+ gap: 2px;
+}
+
+.admin-user-copy strong {
+ overflow: hidden;
+ font-size: 12px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.admin-user-copy small {
+ color: var(--text-muted);
+ font-size: 10px;
}
.logout-btn {
width: 100%;
- font-size: 13px;
+ font-size: 12px;
}
.main-area {
- flex: 1;
display: flex;
- flex-direction: column;
min-width: 0;
+ flex: 1;
+ flex-direction: column;
}
.topbar {
display: none;
+ min-height: var(--header-height);
align-items: center;
gap: 12px;
- height: var(--header-height);
- padding: 0 16px;
+ padding: 0 14px;
border-bottom: 1px solid var(--border);
- background: var(--bg-secondary);
+ background: rgba(12, 15, 20, 0.88);
+ backdrop-filter: blur(18px);
}
.menu-btn {
- padding: 8px;
- font-size: 18px;
+ display: grid;
+ width: 38px;
+ height: 38px;
+ place-items: center;
+ border: 1px solid var(--border);
+ border-radius: 10px;
+ background: rgba(255, 255, 255, 0.025);
}
.page-title {
- font-size: 16px;
- font-weight: 500;
flex: 1;
+ font-size: 14px;
+ font-weight: 650;
}
.member-link {
- font-size: 13px;
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ padding: 7px 10px;
+ border: 1px solid var(--border);
+ border-radius: 9px;
color: var(--text-secondary);
- text-decoration: none;
- padding: 6px 12px;
- border-radius: 6px;
-}
-.member-link:hover {
- color: var(--accent);
- background: var(--bg-hover);
+ font-size: 12px;
}
.main-content {
flex: 1;
overflow-y: auto;
- padding: 24px;
+ padding: clamp(24px, 3vw, 42px);
+}
+
+.main-content > :deep(*) {
+ max-width: 1480px;
+ margin-right: auto;
+ margin-left: auto;
}
.sidebar-overlay {
@@ -257,28 +402,31 @@ function handleLogout() {
@media (max-width: 768px) {
.sidebar {
position: fixed;
- top: 0;
- left: 0;
- bottom: 0;
+ inset: 0 auto 0 0;
z-index: 100;
transform: translateX(-100%);
- transition: transform 0.3s;
+ transition: transform 280ms var(--ease-spring);
}
+
.sidebar.open {
transform: translateX(0);
}
+
.sidebar-overlay.active {
- display: block;
position: fixed;
inset: 0;
- background: rgba(0, 0, 0, 0.5);
z-index: 99;
+ display: block;
+ background: rgba(3, 4, 6, 0.72);
+ backdrop-filter: blur(6px);
}
+
.topbar {
display: flex;
}
+
.main-content {
- padding: 16px;
+ padding: 20px 14px 28px;
}
}
diff --git a/frontend-admin/src/main.js b/frontend-admin/src/main.js
index aa07ea0..2444452 100644
--- a/frontend-admin/src/main.js
+++ b/frontend-admin/src/main.js
@@ -2,8 +2,11 @@ import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
+import { initializeTheme } from './composables/useTheme'
import './assets/main.css'
+initializeTheme()
+
const app = createApp(App)
app.use(createPinia())
app.use(router)
diff --git a/frontend-admin/src/views/ConversationsView.vue b/frontend-admin/src/views/ConversationsView.vue
index 191adbd..9861786 100644
--- a/frontend-admin/src/views/ConversationsView.vue
+++ b/frontend-admin/src/views/ConversationsView.vue
@@ -73,7 +73,9 @@
class="message"
:class="msg.role"
>
- {{ msg.role === 'user' ? '用户' : 'AI' }}
+
+
+
@@ -91,7 +93,8 @@
rel="noopener"
class="att-link"
>
- {{ documentIcon(att) }} {{ att.name || '附件' }}
+
+ {{ att.name || '附件' }}
@@ -116,6 +119,7 @@
import { ref, computed, onMounted, nextTick, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import api from '@/api'
+import UiIcon from '@/components/UiIcon.vue'
const route = useRoute()
const router = useRouter()
@@ -257,14 +261,6 @@ function getAttachments(msg) {
return list.filter(att => att && typeof att === 'object')
}
-function documentIcon(att) {
- const name = (att?.name || '').toLowerCase()
- const mime = att?.mime || ''
- if (name.endsWith('.pdf') || mime.includes('pdf')) return '📕'
- if (name.endsWith('.doc') || name.endsWith('.docx') || mime.includes('word')) return '📘'
- return '📄'
-}
-
function previewImage(url) {
window.open(url, '_blank')
}
@@ -340,8 +336,8 @@ function formatDate(d) {
}
.conv-item.active {
- background: rgba(99, 102, 241, 0.15);
- border: 1px solid rgba(99, 102, 241, 0.35);
+ background: var(--accent-soft);
+ border: 1px solid rgba(183, 243, 107, 0.28);
}
.conv-item-title {
@@ -459,8 +455,8 @@ function formatDate(d) {
}
.message.user .message-content {
- background: rgba(99, 102, 241, 0.2);
- border: 1px solid rgba(99, 102, 241, 0.3);
+ background: var(--accent-soft);
+ border: 1px solid rgba(183, 243, 107, 0.24);
}
.message-time {
diff --git a/frontend-admin/src/views/DashboardView.vue b/frontend-admin/src/views/DashboardView.vue
index fa15261..b5b5ddb 100644
--- a/frontend-admin/src/views/DashboardView.vue
+++ b/frontend-admin/src/views/DashboardView.vue
@@ -6,26 +6,30 @@
-
-
👥
+
+
{{ stats.users }}
用户总数
-
-
-
💬
+
01
+
+
+
{{ stats.conversations }}
会话总数
-
-
-
📝
+
02
+
+
+
{{ stats.messages }}
消息总数
-
-
-
📈
+
03
+
+
+
{{ stats.today_messages }}
今日消息
-
+
04
+
@@ -33,6 +37,7 @@
diff --git a/frontend-admin/src/views/UsersView.vue b/frontend-admin/src/views/UsersView.vue
index 6d1c2e0..24e24ea 100644
--- a/frontend-admin/src/views/UsersView.vue
+++ b/frontend-admin/src/views/UsersView.vue
@@ -422,7 +422,7 @@ function formatDate(d) {
.perm-tag {
font-size: 12px;
padding: 2px 8px;
- background: rgba(99, 102, 241, 0.15);
+ background: var(--accent-soft);
color: var(--accent);
border-radius: 4px;
}
@@ -433,6 +433,6 @@ function formatDate(d) {
}
.danger {
- color: #ef4444;
+ color: var(--danger);
}
diff --git a/frontend/src/assets/main.css b/frontend/src/assets/main.css
index 1225f64..c271473 100644
--- a/frontend/src/assets/main.css
+++ b/frontend/src/assets/main.css
@@ -1,29 +1,86 @@
-@import 'highlight.js/styles/github.css';
+@import 'highlight.js/styles/github-dark.css';
:root {
- --bg-primary: #f5f7fb;
+ color-scheme: light;
+ --bg-primary: #f7f6f3;
--bg-secondary: #ffffff;
- --bg-tertiary: #f8fafc;
- --bg-hover: #eef3f9;
- --bg-soft: #f2f5f8;
- --text-primary: #152033;
- --text-secondary: #526077;
- --text-muted: #7d899b;
- --accent: #2d66da;
- --accent-hover: #2458c4;
- --accent-soft: #edf4ff;
- --border: #e1e7ef;
- --border-strong: #d4dce7;
- --danger: #dc4c4c;
- --sidebar-width: 268px;
- --header-height: 66px;
- --input-max-width: 920px;
- --content-max-width: 1040px;
- --radius: 18px;
- --radius-sm: 12px;
- --shadow: 0 18px 50px rgba(38, 59, 92, 0.08);
- --shadow-soft: 0 8px 24px rgba(38, 59, 92, 0.06);
- --shadow-composer: 0 14px 38px rgba(38, 59, 92, 0.09);
+ --bg-tertiary: #f0efec;
+ --bg-hover: #ebeae6;
+ --bg-soft: #f3f2ef;
+ --surface-raised: #ffffff;
+ --surface-inset: #f3f2ef;
+ --surface-highlight: rgba(255, 255, 255, 0);
+ --surface-neutral: rgba(36, 37, 34, 0.035);
+ --surface-neutral-hover: rgba(36, 37, 34, 0.065);
+ --glass-header: rgba(255, 255, 255, 0.94);
+ --glass-panel: #ffffff;
+ --grid-color: transparent;
+ --text-primary: #242522;
+ --text-secondary: #666762;
+ --text-muted: #92938e;
+ --accent: #346538;
+ --accent-hover: #2d5831;
+ --accent-strong: #244a28;
+ --accent-soft: #edf3ec;
+ --accent-line: #c8d8c6;
+ --border: #e9e8e4;
+ --border-strong: #d9d8d3;
+ --danger: #9f2f2d;
+ --danger-soft: #fdebec;
+ --success: #346538;
+ --button-primary-bg: #242522;
+ --button-primary-hover: #3a3b37;
+ --button-primary-text: #ffffff;
+ --sidebar-width: 264px;
+ --header-height: 64px;
+ --input-max-width: 940px;
+ --content-max-width: 1080px;
+ --radius: 10px;
+ --radius-sm: 6px;
+ --shadow: 0 0 0 rgba(0, 0, 0, 0);
+ --shadow-soft: 0 0 0 rgba(0, 0, 0, 0);
+ --shadow-composer: 0 0 0 rgba(0, 0, 0, 0);
+ --shadow-inset: rgba(36, 37, 34, 0.04);
+ --shadow-control: rgba(36, 37, 34, 0.04);
+ --ease-spring: cubic-bezier(0.22, 1, 0.36, 1);
+}
+
+:root[data-theme="dark"] {
+ color-scheme: dark;
+ --bg-primary: #151615;
+ --bg-secondary: #1d1e1c;
+ --bg-tertiary: #252623;
+ --bg-hover: #2b2c29;
+ --bg-soft: #20211f;
+ --surface-raised: #252623;
+ --surface-inset: #191a18;
+ --surface-highlight: rgba(255, 255, 255, 0);
+ --surface-neutral: rgba(255, 255, 255, 0.035);
+ --surface-neutral-hover: rgba(255, 255, 255, 0.07);
+ --glass-header: rgba(29, 30, 28, 0.96);
+ --glass-panel: #1d1e1c;
+ --grid-color: transparent;
+ --text-primary: #f3f2ee;
+ --text-secondary: #b1b2ac;
+ --text-muted: #777873;
+ --accent: #a8c4a4;
+ --accent-hover: #bdd2ba;
+ --accent-strong: #c7ddc3;
+ --accent-soft: #263127;
+ --accent-line: #3f5641;
+ --border: #2e302d;
+ --border-strong: #3c3e3a;
+ --danger: #e6a09e;
+ --danger-soft: #3a2424;
+ --success: #a8c4a4;
+ --button-primary-bg: #f3f2ee;
+ --button-primary-hover: #d9d8d3;
+ --button-primary-text: #1b1c1a;
+ --shadow: 0 0 0 rgba(0, 0, 0, 0);
+ --shadow-soft: 0 0 0 rgba(0, 0, 0, 0);
+ --shadow-composer: 0 0 0 rgba(0, 0, 0, 0);
+ --shadow-inset: rgba(0, 0, 0, 0.08);
+ --shadow-control: rgba(0, 0, 0, 0.06);
}
* {
@@ -32,11 +89,18 @@
box-sizing: border-box;
}
-html, body, #app {
+html {
+ background: var(--bg-primary);
+ scroll-behavior: smooth;
+}
+
+html,
+body,
+#app {
+ width: 100%;
height: 100%;
min-height: 100dvh;
- font-family: "Microsoft YaHei UI", "PingFang SC", "Segoe UI", sans-serif;
- background: var(--bg-primary);
+ font-family: "SF Pro Display", "Segoe UI Variable", "Helvetica Neue", "HarmonyOS Sans SC", "Microsoft YaHei UI", "PingFang SC", sans-serif;
color: var(--text-primary);
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
@@ -44,11 +108,26 @@ html, body, #app {
body {
overflow: hidden;
+ background: var(--bg-primary);
+}
+
+body::before {
+ display: none;
+}
+
+#app {
+ position: relative;
+ z-index: 1;
}
a {
color: var(--accent);
text-decoration: none;
+ transition: color 180ms ease;
+}
+
+a:hover {
+ color: var(--accent-hover);
}
button {
@@ -59,25 +138,52 @@ button {
font: inherit;
}
+button,
+a,
+input,
+select,
+textarea,
+summary {
+ -webkit-tap-highlight-color: transparent;
+}
+
button:focus-visible,
a:focus-visible,
select:focus-visible,
textarea:focus-visible,
-input:focus-visible {
- outline: 2px solid rgba(45, 102, 218, 0.72);
- outline-offset: 2px;
+input:focus-visible,
+summary:focus-visible {
+ outline: 2px solid var(--accent);
+ outline-offset: 3px;
}
-input, textarea, select {
- font: inherit;
- color: inherit;
- background: transparent;
+input,
+textarea,
+select {
border: none;
outline: none;
+ background: transparent;
+ color: inherit;
+ font: inherit;
+}
+
+input::placeholder,
+textarea::placeholder {
+ color: var(--text-muted);
+}
+
+select {
+ color-scheme: inherit;
+}
+
+::selection {
+ background: rgba(183, 243, 107, 0.26);
+ color: #fff;
}
::-webkit-scrollbar {
- width: 6px;
+ width: 7px;
+ height: 7px;
}
::-webkit-scrollbar-track {
@@ -85,99 +191,172 @@ input, textarea, select {
}
::-webkit-scrollbar-thumb {
- background: #ced7e3;
+ border: 2px solid transparent;
border-radius: 999px;
+ background: var(--border-strong);
+ background-clip: padding-box;
+}
+
+::-webkit-scrollbar-thumb:hover {
+ background: var(--text-muted);
+ background-clip: padding-box;
}
.btn {
+ position: relative;
display: inline-flex;
+ min-height: 40px;
align-items: center;
justify-content: center;
gap: 8px;
- padding: 10px 20px;
- border-radius: 999px;
- font-size: 14px;
- font-weight: 500;
- transition: color 0.16s ease, background 0.16s ease, border-color 0.16s ease, transform 0.16s ease;
+ padding: 9px 17px;
+ overflow: hidden;
+ border: 1px solid var(--border-strong);
+ border-radius: 12px;
+ background: linear-gradient(180deg, var(--surface-highlight), var(--surface-neutral));
+ color: var(--text-primary);
+ font-size: 13px;
+ font-weight: 650;
+ letter-spacing: 0.01em;
+ box-shadow: inset 0 1px 0 var(--surface-highlight), 0 6px 16px var(--shadow-control);
+ transition: transform 180ms var(--ease-spring), border-color 180ms ease, background 180ms ease, box-shadow 180ms ease, color 180ms ease;
+}
+
+.btn::after {
+ position: absolute;
+ inset: 0;
+ pointer-events: none;
+ content: "";
+ background: linear-gradient(110deg, transparent 25%, rgba(255, 255, 255, 0.09) 47%, transparent 68%);
+ transform: translateX(-125%);
+ transition: transform 520ms var(--ease-spring);
+}
+
+.btn:hover::after {
+ transform: translateX(125%);
+}
+
+.btn:hover {
+ border-color: rgba(183, 243, 107, 0.28);
+ background: linear-gradient(180deg, var(--surface-raised), var(--surface-neutral-hover));
+ box-shadow: inset 0 1px 0 var(--surface-highlight), 0 9px 24px var(--shadow-control);
+ transform: translateY(-1px);
}
.btn:active {
- transform: scale(0.97);
+ transform: translateY(2px) scale(0.985);
+ box-shadow: inset 0 2px 5px rgba(0, 0, 0, 0.2);
}
.btn-primary {
- background: var(--accent);
- color: white;
+ border-color: rgba(183, 243, 107, 0.62);
+ background: linear-gradient(180deg, var(--accent-hover), var(--accent));
+ color: #11150e;
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.4), 0 5px 0 #577c2f, 0 12px 24px rgba(126, 183, 67, 0.14);
}
.btn-primary:hover {
- background: var(--accent-hover);
+ border-color: var(--accent-hover);
+ background: linear-gradient(180deg, #d0ff97, var(--accent-hover));
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.52), 0 6px 0 #5e8435, 0 15px 30px rgba(126, 183, 67, 0.2);
}
-.btn-primary:disabled {
- opacity: 0.5;
+.btn-primary:active {
+ transform: translateY(4px) scale(0.99);
+ box-shadow: inset 0 2px 6px rgba(68, 96, 37, 0.2), 0 1px 0 #577c2f;
+}
+
+.btn-primary:disabled,
+.btn:disabled {
cursor: not-allowed;
+ opacity: 0.42;
+ transform: none;
+ box-shadow: none;
}
.btn-ghost {
- background: #fff;
+ background: var(--surface-neutral);
color: var(--text-secondary);
- border: 1px solid var(--border);
}
.btn-ghost:hover {
- background: var(--bg-hover);
color: var(--text-primary);
}
-.btn-danger {
+.btn-danger,
+.danger {
color: var(--danger);
}
-.btn-danger:hover {
- background: rgba(239, 68, 68, 0.1);
+.btn-danger:hover,
+.danger:hover {
+ border-color: rgba(255, 126, 121, 0.32);
+ background: var(--danger-soft);
}
.form-group {
- margin-bottom: 16px;
+ margin-bottom: 18px;
}
.form-group label {
display: block;
- margin-bottom: 6px;
- font-size: 14px;
+ margin-bottom: 8px;
color: var(--text-secondary);
+ font-size: 12px;
+ font-weight: 650;
+ letter-spacing: 0.035em;
}
-.form-input {
+.form-input,
+.form-select {
width: 100%;
- padding: 12px 16px;
- background: #fff;
- border: 1px solid var(--border);
- border-radius: 14px;
- font-size: 15px;
- transition: border-color 0.2s, box-shadow 0.2s;
+ min-height: 46px;
+ padding: 11px 14px;
+ border: 1px solid var(--border-strong);
+ border-radius: 12px;
+ background: var(--surface-inset);
+ color: var(--text-primary);
+ font-size: 14px;
+ box-shadow: inset 0 2px 8px var(--shadow-inset), 0 1px 0 var(--surface-highlight);
+ transition: border-color 180ms ease, box-shadow 180ms ease, background 180ms ease;
}
-.form-input:focus {
- border-color: var(--accent);
- box-shadow: 0 0 0 3px rgba(45, 102, 218, 0.11);
+.form-input:hover,
+.form-select:hover {
+ border-color: rgba(237, 244, 232, 0.21);
+}
+
+.form-input:focus,
+.form-select:focus {
+ border-color: var(--accent-line);
+ background: var(--bg-secondary);
+ box-shadow: inset 0 2px 8px var(--shadow-inset), 0 0 0 3px var(--accent-soft), 0 0 22px rgba(93, 149, 42, 0.07);
}
.form-error {
+ margin-top: 10px;
color: var(--danger);
- font-size: 13px;
- margin-top: 12px;
+ font-size: 12px;
+ line-height: 1.55;
+}
+
+input[type="checkbox"],
+input[type="radio"] {
+ width: 18px;
+ height: 18px;
+ flex: 0 0 auto;
+ accent-color: var(--accent);
}
.markdown-body {
- line-height: 1.7;
+ color: var(--text-primary);
font-size: 15px;
+ line-height: 1.78;
word-break: break-word;
}
.markdown-body p {
- margin-bottom: 12px;
+ margin-bottom: 13px;
}
.markdown-body p:last-child {
@@ -185,75 +364,91 @@ input, textarea, select {
}
.markdown-body br {
- content: '';
display: block;
margin-top: 10px;
+ content: "";
}
.markdown-body pre {
- background: #f8fafc;
- border: 1px solid var(--border);
- border-radius: 14px;
+ margin: 14px 0;
padding: 16px;
overflow-x: auto;
- margin: 12px 0;
+ border: 1px solid var(--border);
+ border-radius: 14px;
+ background: #080a0d;
+ box-shadow: inset 0 1px 8px rgba(0, 0, 0, 0.45);
}
.markdown-body code {
- font-family: 'SF Mono', Monaco, Consolas, monospace;
+ font-family: "Cascadia Code", "SFMono-Regular", Consolas, monospace;
font-size: 13px;
}
.markdown-body :not(pre) > code {
- background: #eef2ff;
padding: 2px 6px;
+ border: 1px solid var(--border);
border-radius: 6px;
+ background: var(--bg-tertiary);
+ color: var(--accent-strong);
}
-.markdown-body ul, .markdown-body ol {
- padding-left: 24px;
+.markdown-body ul,
+.markdown-body ol {
margin-bottom: 12px;
+ padding-left: 24px;
}
.markdown-body blockquote {
- border-left: 3px solid var(--accent);
- padding-left: 16px;
+ margin: 14px 0;
+ padding: 3px 0 3px 16px;
+ border-left: 2px solid var(--accent);
color: var(--text-secondary);
- margin: 12px 0;
}
.markdown-body table {
- border-collapse: collapse;
width: 100%;
- margin: 12px 0;
+ margin: 14px 0;
+ border-collapse: separate;
+ border-spacing: 0;
+ overflow: hidden;
+ border: 1px solid var(--border);
+ border-radius: 12px;
}
-.markdown-body th, .markdown-body td {
- border: 1px solid var(--border);
- padding: 8px 12px;
+.markdown-body th,
+.markdown-body td {
+ padding: 9px 12px;
+ border-right: 1px solid var(--border);
+ border-bottom: 1px solid var(--border);
text-align: left;
}
.markdown-body th {
- background: var(--bg-soft);
+ background: var(--bg-tertiary);
+ color: var(--text-secondary);
}
-.markdown-body a {
- color: var(--accent);
+.markdown-body th:last-child,
+.markdown-body td:last-child {
+ border-right: none;
+}
+
+.markdown-body tr:last-child td {
+ border-bottom: none;
}
.markdown-body img {
max-width: 100%;
- border-radius: 8px;
+ border-radius: 12px;
}
.sidebar-overlay {
display: none;
position: fixed;
inset: 0;
- background: rgba(21, 32, 51, 0.34);
- backdrop-filter: blur(2px);
z-index: 90;
+ background: rgba(3, 4, 6, 0.66);
+ backdrop-filter: blur(5px);
}
@media (max-width: 768px) {
@@ -269,5 +464,647 @@ input, textarea, select {
scroll-behavior: auto !important;
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
+ transition-duration: 0.01ms !important;
}
}
+
+html.theme-transitioning *,
+html.theme-transitioning *::before,
+html.theme-transitioning *::after {
+ transition-property: background-color, background, border-color, color, box-shadow, fill, stroke !important;
+ transition-duration: 220ms !important;
+ transition-timing-function: ease !important;
+}
+
+/* Light theme surfaces for component styles that intentionally stay dark by default. */
+html[data-theme="light"] .auth-story {
+ background:
+ linear-gradient(135deg, rgba(93, 149, 42, 0.09), transparent 45%),
+ repeating-linear-gradient(135deg, rgba(38, 50, 32, 0.025) 0 1px, transparent 1px 14px),
+ #f0f3eb;
+}
+
+html[data-theme="light"] .auth-shell {
+ box-shadow: inset 0 1px 0 #fff, var(--shadow);
+}
+
+html[data-theme="light"] .chat-main::before {
+ background-image: radial-gradient(rgba(38, 50, 32, 0.09) 0.7px, transparent 0.7px);
+}
+
+html[data-theme="light"] .chat-header {
+ background: var(--glass-header);
+ box-shadow: 0 12px 30px rgba(44, 57, 37, 0.06);
+}
+
+html[data-theme="light"] .menu-btn,
+html[data-theme="light"] .toolbar-primary > .icon-btn,
+html[data-theme="light"] .icon-btn,
+html[data-theme="light"] .agent-trigger,
+html[data-theme="light"] .model-picker,
+html[data-theme="light"] .mode-switch {
+ background: var(--surface-neutral);
+}
+
+html[data-theme="light"] .sidebar {
+ background:
+ radial-gradient(circle at 40% -12%, rgba(93, 149, 42, 0.1), transparent 24%),
+ rgba(250, 251, 248, 0.97);
+ box-shadow: 18px 0 50px rgba(44, 57, 37, 0.08);
+}
+
+html[data-theme="light"] .sidebar-footer {
+ background: rgba(31, 43, 25, 0.025);
+}
+
+html[data-theme="light"] .conversation-item:hover,
+html[data-theme="light"] .conversation-icon-wrap {
+ background: var(--surface-neutral-hover);
+}
+
+html[data-theme="light"] .scroll-bottom-btn {
+ background: var(--glass-panel);
+ box-shadow: inset 0 1px 0 #fff, 0 12px 34px rgba(44, 57, 37, 0.12);
+}
+
+html[data-theme="light"] .scroll-bottom-btn:hover {
+ background: var(--bg-secondary);
+ box-shadow: inset 0 1px 0 #fff, 0 14px 38px rgba(44, 57, 37, 0.14);
+}
+
+html[data-theme="light"] .loading-state span,
+html[data-theme="light"] .image-loading-grid i {
+ background: linear-gradient(100deg, #e9ede5 15%, #f7f9f5 38%, #e9ede5 62%);
+ background-size: 220% 100%;
+}
+
+html[data-theme="light"] .message-avatar {
+ box-shadow: inset 0 1px 0 #fff, 0 7px 18px rgba(44, 57, 37, 0.1);
+}
+
+html[data-theme="light"] .message.user .message-content {
+ background:
+ linear-gradient(145deg, rgba(93, 149, 42, 0.12), rgba(93, 149, 42, 0.045)),
+ var(--bg-secondary);
+ box-shadow: inset 0 1px 0 #fff, 0 10px 28px rgba(44, 57, 37, 0.09);
+}
+
+html[data-theme="light"] .message.user .message-content pre,
+html[data-theme="light"] .message.user .message-content :not(pre) > code {
+ background: #182019;
+ color: #e9f7dc;
+}
+
+html[data-theme="light"] .generation-progress {
+ background: rgba(255, 255, 255, 0.92);
+ box-shadow: 0 7px 20px rgba(44, 57, 37, 0.11);
+}
+
+html[data-theme="light"] .att-image,
+html[data-theme="light"] .att-video {
+ box-shadow: 0 18px 42px rgba(44, 57, 37, 0.15);
+}
+
+html[data-theme="light"] .att-document {
+ box-shadow: inset 0 1px 0 #fff, 0 8px 22px rgba(44, 57, 37, 0.09);
+}
+
+html[data-theme="light"] .chat-input-area {
+ background: linear-gradient(to top, rgba(245, 247, 242, 0.99) 76%, rgba(245, 247, 242, 0));
+}
+
+html[data-theme="light"] .attachment-preview,
+html[data-theme="light"] .emoji-panel,
+html[data-theme="light"] .drop-card,
+html[data-theme="light"] .notification-card {
+ background: var(--glass-panel);
+}
+
+html[data-theme="light"] .input-wrapper {
+ background:
+ radial-gradient(circle at 96% 0%, rgba(93, 149, 42, 0.06), transparent 28%),
+ var(--bg-secondary);
+ box-shadow: inset 0 1px 0 #fff, inset 0 -1px 0 rgba(31, 43, 25, 0.08), var(--shadow-composer);
+}
+
+html[data-theme="light"] .input-wrapper:hover {
+ border-color: rgba(31, 43, 25, 0.2);
+}
+
+html[data-theme="light"] .input-wrapper:focus-within,
+html[data-theme="light"] .input-wrapper.image-mode:focus-within {
+ box-shadow: inset 0 1px 0 #fff, 0 0 0 3px var(--accent-soft), 0 22px 54px rgba(44, 57, 37, 0.15);
+}
+
+html[data-theme="light"] .mode-switch {
+ box-shadow: inset 0 2px 7px rgba(31, 43, 25, 0.09);
+}
+
+html[data-theme="light"] .mode-tab.active {
+ box-shadow: inset 0 1px 0 #fff, 0 3px 9px rgba(44, 57, 37, 0.11);
+}
+
+html[data-theme="light"] .voice-trigger {
+ background: linear-gradient(180deg, #fff, #e8ede4);
+ color: var(--accent-strong);
+ box-shadow: inset 0 1px 0 #fff, 0 4px 0 #cbd3c5, 0 10px 22px rgba(44, 57, 37, 0.11);
+}
+
+html[data-theme="light"] .voice-trigger:hover:not(:disabled) {
+ background: linear-gradient(180deg, #fff, #eef4e9);
+ box-shadow: inset 0 1px 0 #fff, 0 5px 0 #c4cec0, 0 13px 26px rgba(44, 57, 37, 0.14);
+}
+
+html[data-theme="light"] .voice-dialog {
+ background:
+ radial-gradient(circle at 50% -4%, rgba(93, 149, 42, 0.09), transparent 32%),
+ var(--bg-secondary);
+}
+
+html[data-theme="light"] .voice-orb {
+ background: #eff5e9;
+ box-shadow: 0 18px 46px rgba(72, 107, 44, 0.14), inset 0 0 0 1px #fff;
+}
+
+html[data-theme="light"] .notification-card {
+ box-shadow: inset 0 1px 0 #fff, 0 20px 56px rgba(44, 57, 37, 0.16);
+}
+
+html[data-theme="light"] .notification-close,
+html[data-theme="light"] .copy-detail {
+ background: var(--surface-neutral);
+}
+
+html[data-theme="light"] .workbench {
+ background: var(--bg-primary);
+ color: var(--text-primary);
+}
+
+html[data-theme="light"] .workbench-sidebar,
+html[data-theme="light"] .tool-bar,
+html[data-theme="light"] .canvas-footer {
+ border-color: var(--border);
+ background: var(--bg-secondary);
+ color: var(--text-primary);
+}
+
+html[data-theme="light"] .suggestions button {
+ background: var(--surface-neutral);
+}
+
+html[data-theme="light"] .composer,
+html[data-theme="light"] .workflow-panel {
+ background: var(--bg-secondary);
+ box-shadow: inset 0 1px 0 #fff, 0 16px 40px rgba(44, 57, 37, 0.12);
+}
+
+html[data-theme="light"] .canvas-shell {
+ background-color: #edf0ea;
+ background-image: radial-gradient(rgba(38, 50, 32, 0.1) 0.8px, transparent 0.8px);
+}
+
+html[data-theme="light"] .canvas-stage {
+ background-color: #fff;
+ background-image: linear-gradient(45deg, #e4e8e1 25%, transparent 25%), linear-gradient(-45deg, #e4e8e1 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #e4e8e1 75%), linear-gradient(-45deg, transparent 75%, #e4e8e1 75%);
+}
+
+html[data-theme="light"] .canvas-stage img {
+ box-shadow: 0 24px 64px rgba(44, 57, 37, 0.2);
+}
+
+html[data-theme="light"] .outpaint-frame {
+ background-color: #eef2eb;
+ background-image: radial-gradient(circle, #ccd4c7 1px, transparent 1.1px);
+ box-shadow: inset 0 0 0 1px #fff;
+}
+
+html[data-theme="light"] .restore-progress {
+ background: rgba(255, 255, 255, 0.96);
+ box-shadow: 0 18px 48px rgba(44, 57, 37, 0.16);
+}
+
+@media (prefers-reduced-motion: reduce) {
+ html.theme-transitioning *,
+ html.theme-transitioning *::before,
+ html.theme-transitioning *::after {
+ transition-duration: 0.01ms !important;
+ }
+}
+
+/* Flat editorial redesign */
+html[data-theme] body,
+html[data-theme] .auth-page,
+html[data-theme] .chat-layout,
+html[data-theme] .chat-main,
+html[data-theme] .message-list {
+ background: var(--bg-primary) !important;
+ background-image: none !important;
+}
+
+html[data-theme] :is(
+ .auth-shell,
+ .auth-logo,
+ .brand-badge,
+ .new-chat-btn,
+ .scroll-bottom-btn,
+ .welcome-icon,
+ .message-avatar,
+ .message-content,
+ .att-image,
+ .att-video,
+ .att-document,
+ .attachment-preview,
+ .input-wrapper,
+ .mode-switch,
+ .mode-tab,
+ .emoji-panel,
+ .send-btn,
+ .voice-trigger,
+ .voice-dialog,
+ .voice-orb,
+ .drop-card,
+ .notification-card,
+ .composer,
+ .workflow-panel,
+ .canvas-stage img,
+ .restore-progress
+) {
+ box-shadow: none !important;
+}
+
+html[data-theme] .btn {
+ min-height: 38px;
+ border: 1px solid var(--border-strong);
+ border-radius: 6px;
+ background: var(--bg-secondary);
+ background-image: none;
+ box-shadow: none;
+ transition: background 160ms ease, border-color 160ms ease, color 160ms ease, transform 160ms ease;
+}
+
+html[data-theme] .btn::after {
+ display: none;
+}
+
+html[data-theme] .btn:hover {
+ border-color: var(--border-strong);
+ background: var(--bg-hover);
+ box-shadow: none;
+ transform: none;
+}
+
+html[data-theme] .btn:active {
+ transform: scale(0.98);
+}
+
+html[data-theme] :is(.btn-primary, .new-chat-btn, .send-btn) {
+ border-color: var(--button-primary-bg) !important;
+ border-radius: 6px;
+ background: var(--button-primary-bg) !important;
+ background-image: none !important;
+ color: var(--button-primary-text) !important;
+ box-shadow: none !important;
+}
+
+html[data-theme] :is(.btn-primary, .new-chat-btn, .send-btn):hover:not(:disabled) {
+ border-color: var(--button-primary-hover) !important;
+ background: var(--button-primary-hover) !important;
+ box-shadow: none !important;
+ transform: none;
+}
+
+html[data-theme] :is(.btn-primary, .new-chat-btn, .send-btn):active:not(:disabled) {
+ box-shadow: none !important;
+ transform: scale(0.98);
+}
+
+html[data-theme] :is(.form-input, .form-select) {
+ min-height: 44px;
+ border-color: var(--border-strong);
+ border-radius: 6px;
+ background: var(--bg-secondary);
+ box-shadow: none;
+}
+
+html[data-theme] :is(.form-input, .form-select):focus {
+ border-color: var(--text-primary);
+ background: var(--bg-secondary);
+ box-shadow: 0 0 0 2px var(--accent-soft);
+}
+
+html[data-theme] .auth-shell {
+ border-color: var(--border-strong);
+ border-radius: 10px;
+ background: var(--bg-secondary);
+}
+
+html[data-theme] .auth-shell::after {
+ display: none;
+}
+
+html[data-theme] .auth-story {
+ border-color: var(--border);
+ background: var(--bg-soft) !important;
+ background-image: none !important;
+}
+
+html[data-theme] .auth-logo {
+ border: 0;
+ border-radius: 8px;
+ background: var(--accent-soft);
+ color: var(--accent-strong);
+}
+
+html[data-theme] .auth-story h1 {
+ font-weight: 700;
+ letter-spacing: -0.055em;
+}
+
+html[data-theme] .auth-eyebrow {
+ color: var(--accent);
+}
+
+html[data-theme] .auth-card {
+ background: var(--bg-secondary) !important;
+ background-image: none !important;
+}
+
+html[data-theme] .auth-status i,
+html[data-theme] .chat-kicker i,
+html[data-theme] .user-online,
+html[data-theme] .agent-live-dot,
+html[data-theme] .typing-indicator span {
+ box-shadow: none !important;
+}
+
+html[data-theme] .sidebar {
+ border-color: var(--border);
+ background: var(--bg-secondary) !important;
+ background-image: none !important;
+ box-shadow: none !important;
+ backdrop-filter: none;
+}
+
+html[data-theme] .sidebar::after,
+html[data-theme] .chat-main::before {
+ display: none;
+}
+
+html[data-theme] .brand-badge {
+ border: 0;
+ border-radius: 6px;
+ background: var(--button-primary-bg);
+ color: var(--button-primary-text);
+}
+
+html[data-theme] .new-chat-btn {
+ min-height: 40px;
+ font-weight: 650;
+}
+
+html[data-theme] .conversation-item {
+ min-height: 40px;
+ border-radius: 6px;
+}
+
+html[data-theme] .conversation-item:hover {
+ border-color: transparent;
+ background: var(--bg-hover);
+ transform: none;
+}
+
+html[data-theme] .conversation-item.active {
+ border-color: transparent;
+ background: var(--accent-soft);
+ background-image: none;
+ color: var(--accent-strong);
+}
+
+html[data-theme] .conversation-icon-wrap,
+html[data-theme] .avatar {
+ border-radius: 6px;
+ background: var(--bg-tertiary);
+}
+
+html[data-theme] .sidebar-footer {
+ background: var(--bg-secondary);
+}
+
+html[data-theme] .chat-header {
+ background: var(--bg-secondary);
+ box-shadow: none;
+ backdrop-filter: none;
+}
+
+html[data-theme] .menu-btn {
+ border-radius: 6px;
+ background: var(--bg-secondary);
+}
+
+html[data-theme] .welcome-panel::before {
+ box-shadow: none;
+}
+
+html[data-theme] .welcome-icon {
+ border: 0;
+ border-radius: 8px;
+ background: var(--accent-soft);
+ color: var(--accent-strong);
+}
+
+html[data-theme] .welcome h2 {
+ font-weight: 700;
+ letter-spacing: -0.045em;
+}
+
+html[data-theme] .scroll-bottom-btn {
+ border-radius: 6px;
+ background: var(--bg-secondary);
+}
+
+html[data-theme] .loading-state span,
+html[data-theme] .image-loading-grid i {
+ background: var(--bg-tertiary) !important;
+ background-image: none !important;
+}
+
+html[data-theme] .message-avatar {
+ border: 0;
+ border-radius: 6px;
+ background: var(--bg-tertiary);
+}
+
+html[data-theme] .message.assistant .message-avatar {
+ background: var(--accent-soft);
+ color: var(--accent-strong);
+}
+
+html[data-theme] .message.user .message-content {
+ border-color: transparent;
+ border-radius: 8px;
+ background: var(--accent-soft);
+ background-image: none;
+}
+
+html[data-theme] .message-action-btn {
+ border-radius: 4px;
+}
+
+html[data-theme] .message-action-btn:hover,
+html[data-theme] .message-action-btn:focus-visible {
+ border-color: transparent;
+ background: var(--bg-hover);
+}
+
+html[data-theme] .att-document {
+ border-radius: 8px;
+ background: var(--bg-secondary);
+}
+
+html[data-theme] .generation-progress {
+ border-radius: 4px;
+ background: var(--bg-secondary);
+ box-shadow: none;
+}
+
+html[data-theme] .chat-input-area {
+ background: var(--bg-primary) !important;
+ background-image: none !important;
+}
+
+html[data-theme] .attachment-preview {
+ border-radius: 8px;
+ background: var(--bg-secondary);
+}
+
+html[data-theme] :is(.preview-item, .preview-file, .preview-uploading) {
+ border-color: var(--border) !important;
+ border-radius: 6px !important;
+ background: var(--bg-tertiary) !important;
+}
+
+html[data-theme] .input-wrapper {
+ border-color: var(--border-strong);
+ border-radius: 10px;
+ background: var(--bg-secondary) !important;
+ background-image: none !important;
+}
+
+html[data-theme] .input-wrapper:hover {
+ border-color: var(--text-muted);
+}
+
+html[data-theme] .input-wrapper:focus-within,
+html[data-theme] .input-wrapper.image-mode:focus-within {
+ border-color: var(--text-primary);
+ box-shadow: 0 0 0 2px var(--accent-soft) !important;
+}
+
+html[data-theme] :is(.icon-btn, .agent-trigger, .model-picker, .mode-switch) {
+ border-color: var(--border);
+ border-radius: 6px;
+ background: var(--bg-secondary);
+ box-shadow: none;
+}
+
+html[data-theme] :is(.icon-btn, .agent-trigger, .model-picker):hover {
+ border-color: var(--border-strong);
+ background: var(--bg-hover);
+ color: var(--text-primary);
+}
+
+html[data-theme] .mode-tab {
+ border-radius: 4px;
+}
+
+html[data-theme] .mode-tab.active {
+ border-color: var(--border);
+ background: var(--bg-secondary);
+ color: var(--text-primary);
+ box-shadow: none;
+}
+
+html[data-theme] .emoji-panel,
+html[data-theme] .drop-card,
+html[data-theme] .voice-dialog,
+html[data-theme] .notification-card {
+ border-radius: 10px;
+ background: var(--bg-secondary) !important;
+ background-image: none !important;
+}
+
+html[data-theme] .voice-trigger {
+ border-color: var(--border-strong);
+ border-radius: 6px;
+ background: var(--bg-secondary) !important;
+ background-image: none !important;
+ color: var(--text-primary);
+}
+
+html[data-theme] .voice-trigger:hover:not(:disabled) {
+ border-color: var(--text-muted);
+ background: var(--bg-hover) !important;
+ box-shadow: none !important;
+}
+
+html[data-theme] .voice-orb {
+ border: 1px solid var(--accent-line);
+ background: var(--accent-soft);
+}
+
+html[data-theme] .notification-card::before {
+ box-shadow: none;
+}
+
+html[data-theme] .notification-close,
+html[data-theme] .copy-detail {
+ border-radius: 4px;
+ background: var(--bg-tertiary);
+}
+
+html[data-theme] .workbench {
+ background: var(--bg-primary);
+}
+
+html[data-theme] :is(.workbench-sidebar, .result-strip button img, .outpaint-frame) {
+ box-shadow: none !important;
+}
+
+html[data-theme] :is(.workbench-sidebar, .tool-bar, .canvas-footer) {
+ background: var(--bg-secondary);
+}
+
+html[data-theme] :is(.composer, .workflow-panel) {
+ border-color: var(--border-strong);
+ border-radius: 8px;
+ background: var(--bg-secondary);
+}
+
+html[data-theme] :is(.composer-footer button, .workflow-command > button, .primary-workflow-button) {
+ border-color: var(--button-primary-bg);
+ border-radius: 6px;
+ background: var(--button-primary-bg);
+ background-image: none;
+ color: var(--button-primary-text);
+ box-shadow: none;
+}
+
+html[data-theme] .canvas-shell {
+ background: var(--bg-tertiary);
+ background-image: none;
+}
+
+html[data-theme] .canvas-stage {
+ background: var(--bg-secondary) !important;
+ background-image: none !important;
+}
+
+html[data-theme] .outpaint-frame {
+ background-color: var(--bg-tertiary);
+ background-image: none;
+}
+
+html[data-theme] .restore-progress {
+ border-radius: 8px;
+ background: var(--bg-secondary);
+}
diff --git a/frontend/src/components/ChatInput.vue b/frontend/src/components/ChatInput.vue
index 75d98b8..f546673 100644
--- a/frontend/src/components/ChatInput.vue
+++ b/frontend/src/components/ChatInput.vue
@@ -22,6 +22,77 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ voicePersonaName }} · 语音对话
+
{{ voiceStatusTitle }}
+
{{ voiceStatusDescription }}
+
+
+
+
+
你
+
{{ voiceLastUser }}
+
+
+
{{ voicePersonaName }}
+
{{ voiceLastAssistant }}
+
+
+ {{ voicePersonaGreeting }}
+
+
+
+
+
+
+
+ {{ voiceMicrophoneLabel }}
+
+
+
+ 结束
+
+
+
+
+ 音频由 AI 合成,并非真人录音
+
+
+
+
+
+
@@ -160,6 +231,22 @@
+
+
+
+
+
+
+
+
diff --git a/frontend/src/composables/useTheme.js b/frontend/src/composables/useTheme.js
new file mode 100644
index 0000000..3677ee4
--- /dev/null
+++ b/frontend/src/composables/useTheme.js
@@ -0,0 +1,56 @@
+import { computed, ref } from 'vue'
+
+const STORAGE_KEY = 'ai-chat-theme'
+const DEFAULT_THEME = 'light'
+
+function readStoredTheme() {
+ try {
+ return localStorage.getItem(STORAGE_KEY) === 'dark' ? 'dark' : DEFAULT_THEME
+ } catch {
+ return DEFAULT_THEME
+ }
+}
+
+const theme = ref(readStoredTheme())
+let transitionTimer
+
+function applyTheme(value, animate = false) {
+ const nextTheme = value === 'dark' ? 'dark' : 'light'
+ const root = document.documentElement
+
+ if (animate) {
+ root.classList.add('theme-transitioning')
+ window.clearTimeout(transitionTimer)
+ transitionTimer = window.setTimeout(() => root.classList.remove('theme-transitioning'), 260)
+ }
+
+ root.dataset.theme = nextTheme
+ root.style.colorScheme = nextTheme
+ theme.value = nextTheme
+}
+
+export function initializeTheme() {
+ applyTheme(theme.value)
+}
+
+export function useTheme() {
+ const isDark = computed(() => theme.value === 'dark')
+
+ function setTheme(value) {
+ const nextTheme = value === 'dark' ? 'dark' : 'light'
+ applyTheme(nextTheme, true)
+ try {
+ localStorage.setItem(STORAGE_KEY, nextTheme)
+ } catch {
+ // Theme still works for the current session when storage is unavailable.
+ }
+ }
+
+ return { theme, isDark, setTheme }
+}
+
+if (typeof window !== 'undefined') {
+ window.addEventListener('storage', event => {
+ if (event.key === STORAGE_KEY) applyTheme(event.newValue)
+ })
+}
diff --git a/frontend/src/main.js b/frontend/src/main.js
index aa07ea0..2444452 100644
--- a/frontend/src/main.js
+++ b/frontend/src/main.js
@@ -2,8 +2,11 @@ import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
+import { initializeTheme } from './composables/useTheme'
import './assets/main.css'
+initializeTheme()
+
const app = createApp(App)
app.use(createPinia())
app.use(router)
diff --git a/frontend/src/stores/chat.js b/frontend/src/stores/chat.js
index 233ecd3..d3af92f 100644
--- a/frontend/src/stores/chat.js
+++ b/frontend/src/stores/chat.js
@@ -258,6 +258,7 @@ export const useChatStore = defineStore('chat', () => {
attachments,
agent_id: agentId || null,
image_tool: options.imageTool || null,
+ voice_mode: !!options.voiceMode,
stream: true
})
})
@@ -288,6 +289,11 @@ export const useChatStore = defineStore('chat', () => {
const appendVisibleContent = (content) => {
if (!content) return
receivedContent += content
+ try {
+ options.onTextDelta?.(content)
+ } catch {
+ // 展示层或语音层回调失败不应中断主消息流。
+ }
if (!isAgentStream) {
fullContent += content
diff --git a/frontend/src/stores/settings.js b/frontend/src/stores/settings.js
index 784c59e..ca94b84 100644
--- a/frontend/src/stores/settings.js
+++ b/frontend/src/stores/settings.js
@@ -17,6 +17,10 @@ export const useSettingsStore = defineStore('settings', () => {
})
const siteName = ref('AI Chat')
const allowRegister = ref(true)
+ const voicePersona = ref({
+ name: 'AI 客服',
+ greeting: '您好,请问有什么可以帮您?'
+ })
const models = ref([])
const agents = ref([])
const loaded = ref(false)
@@ -27,6 +31,7 @@ export const useSettingsStore = defineStore('settings', () => {
features.value = data.features
siteName.value = data.site_name
allowRegister.value = data.allow_register
+ voicePersona.value = data.voice_persona || voicePersona.value
loaded.value = true
}
@@ -40,5 +45,5 @@ export const useSettingsStore = defineStore('settings', () => {
agents.value = res.data.data || []
}
- return { features, siteName, allowRegister, models, agents, loaded, loadPublic, loadModels, loadAgents }
+ return { features, siteName, allowRegister, voicePersona, models, agents, loaded, loadPublic, loadModels, loadAgents }
})
diff --git a/frontend/src/views/ChatView.vue b/frontend/src/views/ChatView.vue
index db553ae..f65141f 100644
--- a/frontend/src/views/ChatView.vue
+++ b/frontend/src/views/ChatView.vue
@@ -11,13 +11,14 @@