Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion dashboard/src/assets/mdi-subset/materialdesignicons-subset.css
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/* Auto-generated MDI subset – 247 icons */
/* Auto-generated MDI subset – 252 icons */
/* Do not edit manually. Run: pnpm run subset-icons */

@font-face {
Expand Down Expand Up @@ -316,6 +316,10 @@
content: "\F164B";
}

.mdi-database-import::before {
content: "\F095D";
}

.mdi-database-off::before {
content: "\F1640";
}
Expand Down Expand Up @@ -352,6 +356,10 @@
content: "\F01DA";
}

.mdi-earth::before {
content: "\F01E7";
}

.mdi-emoticon::before {
content: "\F0C68";
}
Expand Down Expand Up @@ -652,6 +660,10 @@
content: "\F0375";
}

.mdi-moon-waning-crescent::before {
content: "\F0F65";
}

.mdi-music-note-outline::before {
content: "\F0F74";
}
Expand Down Expand Up @@ -696,6 +708,10 @@
content: "\F0601";
}

.mdi-palette::before {
content: "\F03D8";
}

.mdi-paperclip::before {
content: "\F03E2";
}
Expand Down Expand Up @@ -900,6 +916,10 @@
content: "\F060D";
}

.mdi-sync::before {
content: "\F04E6";
}

.mdi-text::before {
content: "\F09A8";
}
Expand Down
Binary file not shown.
Binary file not shown.
6 changes: 3 additions & 3 deletions dashboard/src/components/chat/Chat.vue
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@
}}</v-icon>
</template>
<v-list-item-title>{{
isDark ? tm("modes.lightMode") : tm("modes.darkMode")
isDark ? tm("modes.light") : tm("modes.dark")
}}</v-list-item-title>
</v-list-item>
</div>
Expand Down Expand Up @@ -848,7 +848,7 @@ watch(transportMode, (mode) => {
localStorage.setItem("chat.transportMode", mode);
});

const isDark = computed(() => customizer.uiTheme === "PurpleThemeDark");
const isDark = computed(() => customizer.isDarkTheme);
const canSend = computed(
() =>
Boolean(draft.value.trim() || stagedFiles.value.length) && !sending.value,
Expand Down Expand Up @@ -1394,7 +1394,7 @@ async function stopCurrentSession() {
}

function toggleTheme() {
customizer.SET_UI_THEME(isDark.value ? "PurpleTheme" : "PurpleThemeDark");
customizer.TOGGLE_DARK_MODE();
}

function formatTime(value: string) {
Expand Down
5 changes: 2 additions & 3 deletions dashboard/src/components/chat/ChatInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -365,9 +365,8 @@ const emit = defineEmits<{
}>();

const { tm } = useModuleI18n("features/chat");
const isDark = computed(
() => useCustomizerStore().uiTheme === "PurpleThemeDark",
);
const customizer = useCustomizerStore();
const isDark = computed(() => customizer.isDarkTheme);

const inputField = ref<HTMLTextAreaElement | null>(null);
const imageInputRef = ref<HTMLInputElement | null>(null);
Expand Down
6 changes: 3 additions & 3 deletions dashboard/src/components/chat/LiveMode.vue
Original file line number Diff line number Diff line change
Expand Up @@ -54,16 +54,16 @@

<script setup lang="ts">
import { ref, computed, onBeforeUnmount, watch } from 'vue';
import { useTheme } from 'vuetify';
import { useCustomizerStore } from '@/stores/customizer';
import { useVADRecording } from '@/composables/useVADRecording';
import SiriOrb from './LiveOrb.vue';

const emit = defineEmits<{
'close': [];
}>();

const theme = useTheme();
const isDark = computed(() => theme.global.current.value.dark);
const customizer = useCustomizerStore();
const isDark = computed(() => customizer.isDarkTheme);

// 使用 VAD Recording composable
const vadRecording = useVADRecording();
Expand Down
2 changes: 1 addition & 1 deletion dashboard/src/components/chat/StandaloneChat.vue
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ const messagesContainer = ref<HTMLElement | null>(null);
const inputRef = ref<InstanceType<typeof ChatInput> | null>(null);
const imagePreview = reactive({ visible: false, url: "" });

const isDark = computed(() => customizer.uiTheme === "PurpleThemeDark");
const isDark = computed(() => customizer.isDarkTheme);
const customMarkdownTags = ["ref"];

const {
Expand Down
6 changes: 3 additions & 3 deletions dashboard/src/components/shared/ReadmeDialog.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script setup>
import { ref, watch, computed, onUnmounted } from "vue";
import { useTheme } from "vuetify";
import { useCustomizerStore } from "@/stores/customizer";
import MarkdownIt from "markdown-it";
import axios from "axios";
import DOMPurify from "dompurify";
Expand Down Expand Up @@ -46,7 +46,6 @@ const props = defineProps({

const emit = defineEmits(["update:show"]);
const { t, locale } = useI18n();
const theme = useTheme();

const content = ref(null);
const error = ref(null);
Expand All @@ -57,7 +56,8 @@ const lastRequestId = ref(0);
const lastRenderId = ref(0);
const scrollContainer = ref(null);
const renderedHtml = ref("");
const isDark = computed(() => theme.global.current.value.dark);
const customizer = useCustomizerStore();
const isDark = computed(() => customizer.isDarkTheme);

const MARKDOWN_SANITIZE_OPTIONS = {
ALLOWED_TAGS: [
Expand Down
178 changes: 178 additions & 0 deletions dashboard/src/components/shared/ThemeCustomizer.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
<template>
<!-- Row 1: Preset selector + theme mode -->
<div class="d-flex flex-wrap align-center ga-4 mb-4 ml-3">
<v-select v-model="selectedThemePreset" :items="presetOptions" :label="tm('theme.customize.preset')" hide-details
variant="outlined" density="compact" style="min-width: 200px; max-width: 280px"
@update:model-value="applyThemePreset" />
<v-btn-toggle v-model="themeMode" mandatory density="compact" color="primary">
<v-btn value="light" size="small">
<v-icon class="mr-1" size="18">mdi-white-balance-sunny</v-icon>
{{ tm("theme.customize.light") }}
</v-btn>
<v-btn value="dark" size="small">
<v-icon class="mr-1" size="18">mdi-moon-waning-crescent</v-icon>
{{ tm("theme.customize.dark") }}
</v-btn>
<v-btn value="auto" size="small">
<v-icon class="mr-1" size="18">mdi-sync</v-icon>
{{ tm("theme.customize.auto") }}
</v-btn>
</v-btn-toggle>
<v-tooltip location="top">
<template #activator="{ props }">
<v-icon v-bind="props" size="16" color="primary" class="ml-1">mdi-help-circle-outline</v-icon>
</template>
<span>{{ tm("theme.customize.autoSwitchDesc") }}</span>
</v-tooltip>
</div>

<!-- Row 2: Color pickers + reset -->
<v-card variant="outlined" class="pa-3">
<div class="text-body-2 text-medium-emphasis mb-3">
{{ tm("theme.customize.colors") }}
</div>
<div class="d-flex flex-wrap align-center ga-4">
<div class="d-flex align-center ga-3">
<v-text-field v-model="primaryColor" type="color" :label="tm('theme.customize.primary')" hide-details
variant="outlined" density="compact" style="width: 140px" />
<div class="color-preview" :style="{ backgroundColor: primaryColor }" />
</div>
<div class="d-flex align-center ga-3">
<v-text-field v-model="secondaryColor" type="color" :label="tm('theme.customize.secondary')" hide-details
variant="outlined" density="compact" style="width: 140px" />
<div class="color-preview" :style="{ backgroundColor: secondaryColor }" />
</div>
<v-btn size="small" variant="tonal" color="primary" @click="resetThemeColors">
<v-icon class="mr-1" size="16">mdi-restore</v-icon>
{{ tm("theme.customize.reset") }}
</v-btn>
</div>
</v-card>
</template>

<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import { useModuleI18n } from "@/i18n/composables";
import { useTheme } from "vuetify";
import { PurpleTheme } from "@/theme/LightTheme";
import {
LIGHT_THEME_NAME,
DARK_THEME_NAME,
themePresets,
} from "@/theme/constants";
import type { ThemeMode } from "@/types/theme";
import { useToastStore } from "@/stores/toast";
import { useCustomizerStore } from "@/stores/customizer";


const { tm } = useModuleI18n("features/settings");
const toastStore = useToastStore();
const theme = useTheme();
const customizer = useCustomizerStore();

// Theme mode toggle (light/dark/auto)
const themeMode = computed({
get() {
if (customizer.autoSwitchTheme) return "auto";
return customizer.isDarkTheme ? "dark" : "light";
},
set(mode: ThemeMode) {
if (mode === "auto") {
customizer.SET_AUTO_SYNC(true);
customizer.APPLY_SYSTEM_THEME();
return;
}

customizer.SET_AUTO_SYNC(false);
const newTheme = mode === "dark" ? DARK_THEME_NAME : LIGHT_THEME_NAME;
customizer.SET_UI_THEME(newTheme);
},
});

const getStoredColor = (key: string, fallback: string) => {
const stored =
typeof window !== "undefined" ? localStorage.getItem(key) : null;
return stored || fallback;
};

const primaryColor = ref(
getStoredColor("themePrimary", PurpleTheme.colors.primary),
);
const secondaryColor = ref(
getStoredColor("themeSecondary", PurpleTheme.colors.secondary),
);

// Get stored preset or default to blue-business name
const selectedThemePreset = ref(
localStorage.getItem("themePreset") || themePresets[0].name,
);

// Simple array for dropdown display
const presetOptions = themePresets.map((p) => p.name);

const applyThemePreset = (presetName: string) => {
const preset = themePresets.find((p) => p.name === presetName);
if (!preset) return;

// Store the preset selection (store by name for display consistency)
localStorage.setItem("themePreset", presetName);
selectedThemePreset.value = presetName;

// Update primary and secondary colors
primaryColor.value = preset.primary;
secondaryColor.value = preset.secondary;
localStorage.setItem("themePrimary", preset.primary);
localStorage.setItem("themeSecondary", preset.secondary);

// Apply to themes
applyThemeColors(preset.primary, preset.secondary);

toastStore.add({
message: tm("theme.customize.presetApplied") || "主题已应用",
color: "success",
});
};

const resolveThemes = () => {
if (theme?.themes?.value) return theme.themes.value as Record<string, any>;
return null;
};

const applyThemeColors = (primary: string, secondary: string) => {
const themes = resolveThemes();
if (!themes) return;
[LIGHT_THEME_NAME, DARK_THEME_NAME].forEach((name) => {
const themeDef = themes[name];
if (!themeDef?.colors) return;
if (primary) themeDef.colors.primary = primary;
if (secondary) themeDef.colors.secondary = secondary;
if (primary && themeDef.colors.darkprimary)
themeDef.colors.darkprimary = primary;
if (secondary && themeDef.colors.darksecondary)
themeDef.colors.darksecondary = secondary;
});
};
Comment on lines +141 to +154
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Directly modifying the themeDef.colors object properties might not reliably trigger reactivity in all Vuetify components if the themes object is not deeply reactive. A more robust approach in Vuetify 3 is to replace the entire colors object or use the theme.themes.value[name].colors = { ... } pattern to ensure the change is propagated correctly. Additionally, darkprimary and darksecondary are not standard Vuetify color keys; ensure these are explicitly defined in your theme configuration if they are intended for use.

const applyThemeColors = (primary: string, secondary: string) => {
  const themes = resolveThemes();
  if (!themes) return;
  [LIGHT_THEME_NAME, DARK_THEME_NAME].forEach((name) => {
    const themeDef = themes[name];
    if (!themeDef?.colors) return;
    
    const newColors = { ...themeDef.colors };
    if (primary) {
      newColors.primary = primary;
      if (newColors.darkprimary !== undefined) newColors.darkprimary = primary;
    }
    if (secondary) {
      newColors.secondary = secondary;
      if (newColors.darksecondary !== undefined) newColors.darksecondary = secondary;
    }
    themeDef.colors = newColors;
  });
};


applyThemeColors(primaryColor.value, secondaryColor.value);

watch(primaryColor, (value) => {
if (!value) return;
localStorage.setItem("themePrimary", value);
applyThemeColors(value, secondaryColor.value);
});

watch(secondaryColor, (value) => {
if (!value) return;
localStorage.setItem("themeSecondary", value);
applyThemeColors(primaryColor.value, value);
});

const resetThemeColors = () => {
primaryColor.value = PurpleTheme.colors.primary;
secondaryColor.value = PurpleTheme.colors.secondary;
localStorage.removeItem("themePrimary");
localStorage.removeItem("themeSecondary");
applyThemeColors(primaryColor.value, secondaryColor.value);
};

</script>
4 changes: 2 additions & 2 deletions dashboard/src/i18n/locales/en-US/features/auth.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"subtitle": "Welcome"
},
"theme": {
"switchToDark": "Switch to Dark Theme",
"switchToLight": "Switch to Light Theme"
"light": "Light Mode",
"dark": "Dark Mode"
}
}
4 changes: 2 additions & 2 deletions dashboard/src/i18n/locales/en-US/features/chat.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@
"confirmDelete": "Are you sure you want to delete \"{name}\"? This action cannot be undone."
},
"modes": {
"darkMode": "Switch to Dark Mode",
"lightMode": "Switch to Light Mode"
"light": "Light Mode",
"dark": "Dark Mode"
},
"shortcuts": {
"help": "Get Help",
Expand Down
Loading
Loading