Skip to content
Merged
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@
"picocolors": "^1.1.1",
"pinia": "^3.0.3",
"prettier": "^3.5.3",
"reka-ui": "^2.5.0",
"reka-ui": "^2.5.1",
"simple-git-hooks": "^2.13.1",
"tailwind-merge": "^3.3.1",
"tailwind-scrollbar-hide": "^4.0.0",
Expand Down
9 changes: 8 additions & 1 deletion scripts/update-shadcn.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,14 @@ const components = [
'drawer',
'combobox',
'slider',
'scroll-area'
'scroll-area',
'button-group',
'empty',
'field',
'input-group',
'item',
'kbd',
'spinner'
]

// 批量更新组件
Expand Down
163 changes: 73 additions & 90 deletions src/renderer/src/components/settings/ShortcutSettings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -27,64 +27,74 @@
<span class="text-sm font-medium">{{ t(shortcut.label) }}</span>
</span>

<div class="shrink-0 min-w-32">
<div class="relative w-full group">
<Button
variant="outline"
class="h-10 min-w-[140px] justify-end relative px-2"
:class="{
'ring-2 ring-primary': recordingShortcutId === shortcut.id && !shortcutError,
'ring-2 ring-destructive': recordingShortcutId === shortcut.id && shortcutError
}"
:disabled="shortcut.disabled"
@click="startRecording(shortcut.id)"
>
<span
v-if="recordingShortcutId === shortcut.id"
class="text-sm"
:class="{ 'text-primary': !shortcutError, 'text-destructive': shortcutError }"
>
<span v-if="shortcutError">
{{ shortcutError }}
</span>
<span v-else-if="tempShortcut">
{{ tempShortcut }}
<span class="text-xs text-muted-foreground">{{
t('settings.shortcuts.pressEnterToSave')
}}</span>
</span>
<span v-else>{{ t('settings.shortcuts.pressKeys') }}</span>
</span>
<span v-else class="text-sm">
<span
v-for="(key, idx) in shortcut.key"
:key="idx"
class="tw-keycap"
:class="{
'font-mono tracking-widest': key === '0'
}"
>
<div class="shrink-0 min-w-[240px]">
<div
class="group flex min-h-[44px] items-center gap-3 rounded-md border bg-background px-3 py-2 transition"
:class="{
'border-primary ring-2 ring-primary/50':
recordingShortcutId === shortcut.id && !shortcutError,
'border-destructive ring-2 ring-destructive/50':
recordingShortcutId === shortcut.id && shortcutError,
'opacity-60': shortcut.disabled
}"
>
<KbdGroup class="flex flex-wrap items-center gap-1">
<template v-if="recordingShortcutId === shortcut.id">
<template v-if="formattedTempShortcut.length">
<Kbd v-for="(key, idx) in formattedTempShortcut" :key="idx">
{{ key }}
</Kbd>
</template>
<Kbd v-else class="text-muted-foreground">...</Kbd>
</template>
<template v-else-if="shortcut.key.length">
<Kbd v-for="(key, idx) in shortcut.key" :key="idx">
{{ key }}
</span>
</span>
<Icon
v-if="recordingShortcutId !== shortcut.id"
icon="lucide:pencil"
class="w-3.5 h-3.5 text-muted-foreground group-hover:opacity-0 transition-opacity"
/>
</Button>

<!-- 清理图标 - 只在hover时显示且不是禁用状态 -->
<Button
v-if="!shortcut.disabled && recordingShortcutId !== shortcut.id"
variant="ghost"
size="sm"
class="absolute right-1 top-1/2 -translate-y-1/2 h-8 w-8 p-0 opacity-0 group-hover:opacity-100 transition-opacity z-10 hover:bg-destructive/10"
@click.stop="clearShortcut(shortcut.id)"
:title="t('settings.shortcuts.clearShortcut')"
</Kbd>
</template>
<Kbd v-else class="text-muted-foreground">—</Kbd>
</KbdGroup>

<div
class="ml-auto flex items-center gap-1 opacity-0 transition-opacity group-hover:opacity-100"
:class="{ 'opacity-100': recordingShortcutId === shortcut.id }"
>
<Icon icon="lucide:x" class="w-4 h-4 text-destructive" />
</Button>
<Button
v-if="!shortcut.disabled"
variant="ghost"
size="icon"
class="h-8 w-8 text-muted-foreground hover:text-primary"
:title="t('common.edit')"
@click.stop="startRecording(shortcut.id)"
>
<Icon icon="lucide:pencil" class="h-4 w-4" />
</Button>
<Button
v-if="!shortcut.disabled && shortcut.key.length"
variant="ghost"
size="icon"
class="h-8 w-8 text-muted-foreground hover:text-destructive"
:title="t('settings.shortcuts.clearShortcut')"
@click.stop="clearShortcut(shortcut.id)"
>
<Icon icon="lucide:x" class="h-4 w-4" />
</Button>
</div>
</div>
<div
v-if="recordingShortcutId === shortcut.id"
class="mt-1 text-xs"
:class="shortcutError ? 'text-destructive' : 'text-muted-foreground'"
>
<span v-if="shortcutError">
{{ shortcutError }}
</span>
<span v-else-if="formattedTempShortcut.length">
{{ t('settings.shortcuts.pressEnterToSave') }}
</span>
<span v-else class="text-primary">
{{ t('settings.shortcuts.pressKeys') }}
</span>
</div>
</div>
</div>
Expand All @@ -105,6 +115,7 @@ import { useShortcutKeyStore } from '@/stores/shortcutKey'
import { useLanguageStore } from '@/stores/language'
import { Button } from '@shadcn/components/ui/button'
import { ScrollArea } from '@shadcn/components/ui/scroll-area'
import { Kbd, KbdGroup } from '@shadcn/components/ui/kbd'
import type { ShortcutKey } from '@shared/presenter'

const { t } = useI18n()
Expand Down Expand Up @@ -254,9 +265,9 @@ const shortcuts = computed(() => {
}
})

const formatShortcut = (_shortcut: string | undefined | null) => {
// 如果 _shortcut 为空,返回空字符串或一个默认值
if (!_shortcut) return ''
const formatShortcut = (_shortcut: string | undefined | null): string[] => {
// 如果 _shortcut 为空,返回空数组
if (!_shortcut) return []

return _shortcut
.replace(
Expand All @@ -270,8 +281,11 @@ const formatShortcut = (_shortcut: string | undefined | null) => {
.replace(/\+/g, ' + ')
.split('+')
.map((k) => k.trim())
.filter(Boolean)
}

const formattedTempShortcut = computed(() => formatShortcut(tempShortcut.value))

const resetShortcutKeys = async () => {
resetLoading.value = true

Expand Down Expand Up @@ -440,34 +454,3 @@ const clearShortcut = async (shortcutId: string) => {
}
}
</script>

<style scoped>
.tw-keycap {
display: inline-flex;
min-width: 1.6rem;
height: 1.6rem;
align-items: center;
justify-content: center;
padding: 0.125rem 0.375rem;
margin: 0 0.125rem;
border-radius: 0.3rem;
font-size: 0.75rem;
line-height: 1rem;
font-weight: 500;
letter-spacing: 0.01em;
vertical-align: middle;
border-width: 1px;
border-style: solid;
box-shadow:
inset 0 -1px 0 rgb(15 23 42 / 0.08),
0 1px 2px rgb(15 23 42 / 0.06);
transition:
color 150ms ease,
background-color 150ms ease,
border-color 150ms ease;
user-select: none;
background-color: hsl(var(--muted));
border-color: hsl(var(--border));
color: hsl(var(--foreground));
}
</style>
67 changes: 62 additions & 5 deletions src/renderer/src/views/PlaygroundTabView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,21 @@
<script setup lang="ts">
import { computed } from 'vue'
import DemoSection from './playground/DemoSection.vue'
import PopoverDemo from './playground/demos/PopoverDemo.vue'
import DialogDemo from './playground/demos/DialogDemo.vue'
import TabsDemo from './playground/demos/TabsDemo.vue'
import AccordionDemo from './playground/demos/AccordionDemo.vue'
import FormDemo from './playground/demos/FormDemo.vue'
import ThinkContentDemo from './playground/demos/ThinkContentDemo.vue'
import ButtonGroupDemo from './playground/demos/ButtonGroupDemo.vue'
import CardDemo from './playground/demos/CardDemo.vue'
import DialogDemo from './playground/demos/DialogDemo.vue'
import EmptyDemo from './playground/demos/EmptyDemo.vue'
import FieldDemo from './playground/demos/FieldDemo.vue'
import FormDemo from './playground/demos/FormDemo.vue'
import InputGroupDemo from './playground/demos/InputGroupDemo.vue'
import ItemDemo from './playground/demos/ItemDemo.vue'
import KbdDemo from './playground/demos/KbdDemo.vue'
import PopoverDemo from './playground/demos/PopoverDemo.vue'
import SelectDemo from './playground/demos/SelectDemo.vue'
import SpinnerDemo from './playground/demos/SpinnerDemo.vue'
import TabsDemo from './playground/demos/TabsDemo.vue'
import ThinkContentDemo from './playground/demos/ThinkContentDemo.vue'

const sections = computed(() => [
{
Expand All @@ -59,6 +66,18 @@ const sections = computed(() => [
description: 'Basic form layout using button, input, textarea, checkbox, and switch.',
componentName: '@shadcn/components/ui',
render: FormDemo
},
{
title: 'Field layouts',
description: 'Compose labels, descriptions, and validation with field primitives.',
componentName: '@shadcn/components/ui/field',
render: FieldDemo
},
{
title: 'Input group',
description: 'Addons, buttons, and helpers aligned with a single control.',
componentName: '@shadcn/components/ui/input-group',
render: InputGroupDemo
}
]
},
Expand Down Expand Up @@ -99,6 +118,12 @@ const sections = computed(() => [
description: 'Popover anchored to trigger with focus management.',
componentName: '@shadcn/components/ui/popover',
render: PopoverDemo
},
{
title: 'Spinner',
description: 'Lightweight loading indicator available in multiple sizes.',
componentName: '@shadcn/components/ui/spinner',
render: SpinnerDemo
}
]
},
Expand All @@ -119,6 +144,12 @@ const sections = computed(() => [
description: 'Collapsible reasoning block used for model thoughts.',
componentName: '@/components/think-content',
render: ThinkContentDemo
},
{
title: 'Item',
description: 'Flexible list row with media, actions, and metadata.',
componentName: '@shadcn/components/ui/item',
render: ItemDemo
}
]
},
Expand All @@ -135,6 +166,32 @@ const sections = computed(() => [
render: SelectDemo
}
]
},
{
title: 'Shortcuts & States',
description: 'Toolbar groups, keyboard hints, and empty states for application scaffolding.',
columns: 2,
component: DemoSection,
demos: [
{
title: 'Button group',
description: 'Organize related actions with toolbar-style groups.',
componentName: '@shadcn/components/ui/button-group',
render: ButtonGroupDemo
},
{
title: 'Keyboard shortcuts',
description: 'Display shortcut helpers with accessible keycaps.',
componentName: '@shadcn/components/ui/kbd',
render: KbdDemo
},
{
title: 'Empty state',
description: 'Pre-built layout for zero-data scenarios.',
componentName: '@shadcn/components/ui/empty',
render: EmptyDemo
}
]
}
])
</script>
Expand Down
39 changes: 39 additions & 0 deletions src/renderer/src/views/playground/demos/ButtonGroupDemo.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<template>
<div class="space-y-4">
<ButtonGroup class="w-full">
<Button variant="ghost" size="sm" class="flex-1">Day</Button>
<Button size="sm" class="flex-1">Week</Button>
<ButtonGroupSeparator />
<Button variant="ghost" size="sm" class="flex-1">Month</Button>
<Button variant="ghost" size="sm" class="flex-1">Year</Button>
</ButtonGroup>

<ButtonGroup orientation="vertical" class="w-full">
<ButtonGroupText class="justify-between">
<span class="text-xs uppercase tracking-wide text-muted-foreground">Team access</span>
<span class="text-xs text-muted-foreground">4 members</span>
</ButtonGroupText>
<Button variant="ghost" class="justify-between">
<span>Marketing</span>
<span class="text-xs text-muted-foreground">Owner</span>
</Button>
<Button variant="ghost" class="justify-between">
<span>Engineering</span>
<span class="text-xs text-muted-foreground">Can edit</span>
</Button>
<Button variant="ghost" class="justify-between">
<span>Support</span>
<span class="text-xs text-muted-foreground">Can view</span>
</Button>
</ButtonGroup>
</div>
</template>
Comment on lines +1 to +30
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Replace all hardcoded UI text with i18n keys.

The template contains numerous hardcoded user-facing strings (button labels "Day", "Week", "Month", "Year", section headers "Team access", "4 members", team names, and role labels). Per coding guidelines, all user-visible strings in src/renderer/src/**/* must use vue-i18n translation keys.

As per coding guidelines.

Apply this diff to the script section:

+import { useI18n } from 'vue-i18n'
+
+const { t } = useI18n()

Then update all hardcoded strings in the template to use {{ t('key') }} pattern. For example:

-      <Button variant="ghost" size="sm" class="flex-1">Day</Button>
-      <Button size="sm" class="flex-1">Week</Button>
+      <Button variant="ghost" size="sm" class="flex-1">{{ t('playground.buttonGroup.day') }}</Button>
+      <Button size="sm" class="flex-1">{{ t('playground.buttonGroup.week') }}</Button>

Apply similar changes to all other user-facing strings and add corresponding keys to your i18n locale files.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/renderer/src/views/playground/demos/ButtonGroupDemo.vue lines 1-30:
template contains hardcoded user-facing strings (Day, Week, Month, Year, Team
access, 4 members, Marketing, Engineering, Support, Owner, Can edit, Can view).
Replace each with vue-i18n lookups (use the t('...') pattern), add a script
section using useI18n (or ensure t is available in the component setup/Options
API), and wire keys such as playground.button.day, playground.button.week,
playground.button.month, playground.button.year, playground.team.title,
playground.team.count, playground.team.marketing, playground.team.engineering,
playground.team.support, playground.role.owner, playground.role.canEdit,
playground.role.canView (or equivalent names consistent with your locale
structure) into your locale JSON/YAML files. Ensure the template uses {{
t('...') }} or v-t where appropriate and update all strings accordingly so no
hardcoded user-visible text remains.


<script setup lang="ts">
import { Button } from '@shadcn/components/ui/button'
import {
ButtonGroup,
ButtonGroupSeparator,
ButtonGroupText
} from '@shadcn/components/ui/button-group'
</script>
30 changes: 30 additions & 0 deletions src/renderer/src/views/playground/demos/EmptyDemo.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<template>
<Empty class="border border-dashed border-muted-foreground/40 bg-muted/20">
<EmptyMedia>
<Icon icon="lucide:inbox" class="w-10 h-10 text-muted-foreground" />
</EmptyMedia>
<EmptyHeader>
<EmptyTitle>No conversations yet</EmptyTitle>
<EmptyDescription>
Start a new thread to keep track of customer questions and follow-ups.
</EmptyDescription>
</EmptyHeader>
<EmptyContent class="flex flex-col sm:flex-row gap-2">
<Button>New conversation</Button>
<Button variant="outline">Import history</Button>
Comment on lines +7 to +14
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Replace hardcoded UI text with i18n keys.

Lines 7-14 contain hardcoded user-facing text including the title "No conversations yet", the description, and button labels "New conversation" and "Import history". Per coding guidelines, all user-visible strings in src/renderer/src/**/* must use vue-i18n translation keys.

As per coding guidelines.

Apply this diff:

+import { useI18n } from 'vue-i18n'
+
+const { t } = useI18n()

Then update the template:

     <EmptyHeader>
-      <EmptyTitle>No conversations yet</EmptyTitle>
+      <EmptyTitle>{{ t('playground.empty.noConversations') }}</EmptyTitle>
       <EmptyDescription>
-        Start a new thread to keep track of customer questions and follow-ups.
+        {{ t('playground.empty.description') }}
       </EmptyDescription>
     </EmptyHeader>
     <EmptyContent class="flex flex-col sm:flex-row gap-2">
-      <Button>New conversation</Button>
-      <Button variant="outline">Import history</Button>
+      <Button>{{ t('playground.empty.newConversation') }}</Button>
+      <Button variant="outline">{{ t('playground.empty.importHistory') }}</Button>
     </EmptyContent>

Add corresponding keys to your i18n locale files.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<EmptyTitle>No conversations yet</EmptyTitle>
<EmptyDescription>
Start a new thread to keep track of customer questions and follow-ups.
</EmptyDescription>
</EmptyHeader>
<EmptyContent class="flex flex-col sm:flex-row gap-2">
<Button>New conversation</Button>
<Button variant="outline">Import history</Button>
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
// …other imports and setup remain unchanged
</script>
<template>
<EmptyHeader>
<EmptyTitle>{{ t('playground.empty.noConversations') }}</EmptyTitle>
<EmptyDescription>
{{ t('playground.empty.description') }}
</EmptyDescription>
</EmptyHeader>
<EmptyContent class="flex flex-col sm:flex-row gap-2">
<Button>{{ t('playground.empty.newConversation') }}</Button>
<Button variant="outline">{{ t('playground.empty.importHistory') }}</Button>
</EmptyContent>
</template>
🤖 Prompt for AI Agents
In src/renderer/src/views/playground/demos/EmptyDemo.vue around lines 7 to 14,
replace the hardcoded UI strings ("No conversations yet", the description, "New
conversation", "Import history") with vue-i18n translation keys (e.g.
$t('playground.empty.title'), $t('playground.empty.description'),
$t('playground.empty.newConversation'), $t('playground.empty.importHistory')) in
the template, update any Button props to use the translation calls, and then add
those keys with appropriate translations to the locale JSON/YAML files used by
the app (e.g. en.json and other locales) so the strings are available at
runtime.

</EmptyContent>
</Empty>
</template>

<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { Button } from '@shadcn/components/ui/button'
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle
} from '@shadcn/components/ui/empty'
</script>
Loading