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 src/renderer/src/components/ModelSelect.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<div class="space-y-2" :dir="langStore.dir">
<Input
v-model="keyword"
class="w-full border-none border-b ring-0 focus-visible:ring-0 rounded-b-none"
class="w-full text-sm border-none border-b ring-0 focus-visible:ring-0 rounded-b-none"
:placeholder="t('model.search.placeholder')"
/>
<div class="flex flex-col max-h-64 overflow-y-auto">
Expand Down
192 changes: 65 additions & 127 deletions src/renderer/src/components/NewThread.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,85 +15,75 @@
:context-length="contextLength"
@send="handleSend"
>
<template #addon-buttons>
<div
key="newThread-model-select"
class="new-thread-model-select overflow-hidden flex items-center h-7 rounded-lg shadow-sm border border-input transition-all duration-300"
:dir="langStore.dir"
>
<Popover v-model:open="modelSelectOpen">
<PopoverTrigger as-child>
<Button
<template #addon-actions>
<Popover v-model:open="modelSelectOpen">
<PopoverTrigger as-child>
<Button
variant="ghost"
class="flex items-center gap-1.5 h-7 px-2 rounded-md text-xs font-semibold text-muted-foreground hover:bg-muted/60 hover:text-foreground dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
size="sm"
>
<ModelIcon
class="w-4 h-4"
:model-id="activeModel.providerId"
:is-dark="themeStore.isDark"
></ModelIcon>
<span class="text-xs font-semibold truncate max-w-[140px] text-foreground">{{
name
}}</span>
<Badge
v-for="tag in activeModel.tags"
:key="tag"
variant="outline"
class="flex border-none rounded-none shadow-none items-center gap-1.5 px-2 h-full"
size="sm"
class="py-0 px-1 rounded-lg text-[10px]"
>
<ModelIcon
class="w-4 h-4"
:model-id="activeModel.providerId"
:is-dark="themeStore.isDark"
></ModelIcon>
<!-- <Icon icon="lucide:message-circle" class="w-5 h-5 text-muted-foreground" /> -->
<h2 class="text-xs font-bold max-w-[150px] truncate">{{ name }}</h2>
<Badge
v-for="tag in activeModel.tags"
:key="tag"
variant="outline"
class="py-0 rounded-lg"
size="sm"
>
{{ t(`model.tags.${tag}`) }}</Badge
>
<Icon icon="lucide:chevron-right" class="w-4 h-4" />
</Button>
</PopoverTrigger>
<PopoverContent align="start" class="p-0 w-80">
<ModelSelect
:type="[ModelType.Chat, ModelType.ImageGeneration]"
@update:model="handleModelUpdate"
/>
</PopoverContent>
</Popover>
<ScrollablePopover
v-model:open="settingsPopoverOpen"
@update:open="handleSettingsPopoverUpdate"
align="start"
content-class="w-80"
:enable-scrollable="true"
>
<template #trigger>
<Button
class="w-7 h-full rounded-none border-none shadow-none transition-all duration-300"
:class="{
'w-0 opacity-0 p-0 overflow-hidden': !showSettingsButton && !isHovering,
'w-7 opacity-100': showSettingsButton || isHovering
}"
size="icon"
variant="outline"
{{ t(`model.tags.${tag}`) }}</Badge
>
<Icon icon="lucide:settings-2" class="w-4 h-4" />
</Button>
</template>
<ChatConfig
v-model:temperature="temperature"
v-model:context-length="contextLength"
v-model:max-tokens="maxTokens"
v-model:system-prompt="systemPrompt"
v-model:artifacts="artifacts"
v-model:thinking-budget="thinkingBudget"
v-model:enable-search="enableSearch"
v-model:forced-search="forcedSearch"
v-model:search-strategy="searchStrategy"
v-model:reasoning-effort="reasoningEffort"
v-model:verbosity="verbosity"
:context-length-limit="contextLengthLimit"
:max-tokens-limit="maxTokensLimit"
:model-id="activeModel?.id"
:provider-id="activeModel?.providerId"
:model-type="activeModel?.type"
<Icon icon="lucide:chevron-right" class="w-4 h-4 text-muted-foreground" />
</Button>
</PopoverTrigger>
<PopoverContent align="end" class="w-80 p-0">
<ModelSelect
:type="[ModelType.Chat, ModelType.ImageGeneration]"
@update:model="handleModelUpdate"
/>
</ScrollablePopover>
</div>
</PopoverContent>
</Popover>
Comment on lines +19 to +51
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 | 🟠 Major

Add accessible labels for the model selection trigger.

The model selection button should include an aria-label for screen readers, especially important for keyboard navigation users.

Apply this diff:

               <Button
                 variant="ghost"
                 class="flex items-center gap-1.5 h-7 px-2 rounded-md text-xs font-semibold text-muted-foreground hover:bg-muted/60 hover:text-foreground dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
                 size="sm"
+                :aria-label="t('model.select.trigger')"
               >

You'll need to add the corresponding i18n key. As per coding guidelines

📝 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
<Popover v-model:open="modelSelectOpen">
<PopoverTrigger as-child>
<Button
variant="ghost"
class="flex items-center gap-1.5 h-7 px-2 rounded-md text-xs font-semibold text-muted-foreground hover:bg-muted/60 hover:text-foreground dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
size="sm"
>
<ModelIcon
class="w-4 h-4"
:model-id="activeModel.providerId"
:is-dark="themeStore.isDark"
></ModelIcon>
<span class="text-xs font-semibold truncate max-w-[140px] text-foreground">{{
name
}}</span>
<Badge
v-for="tag in activeModel.tags"
:key="tag"
variant="outline"
class="flex border-none rounded-none shadow-none items-center gap-1.5 px-2 h-full"
size="sm"
class="py-0 px-1 rounded-lg text-[10px]"
>
<ModelIcon
class="w-4 h-4"
:model-id="activeModel.providerId"
:is-dark="themeStore.isDark"
></ModelIcon>
<!-- <Icon icon="lucide:message-circle" class="w-5 h-5 text-muted-foreground" /> -->
<h2 class="text-xs font-bold max-w-[150px] truncate">{{ name }}</h2>
<Badge
v-for="tag in activeModel.tags"
:key="tag"
variant="outline"
class="py-0 rounded-lg"
size="sm"
>
{{ t(`model.tags.${tag}`) }}</Badge
>
<Icon icon="lucide:chevron-right" class="w-4 h-4" />
</Button>
</PopoverTrigger>
<PopoverContent align="start" class="p-0 w-80">
<ModelSelect
:type="[ModelType.Chat, ModelType.ImageGeneration]"
@update:model="handleModelUpdate"
/>
</PopoverContent>
</Popover>
<ScrollablePopover
v-model:open="settingsPopoverOpen"
@update:open="handleSettingsPopoverUpdate"
align="start"
content-class="w-80"
:enable-scrollable="true"
>
<template #trigger>
<Button
class="w-7 h-full rounded-none border-none shadow-none transition-all duration-300"
:class="{
'w-0 opacity-0 p-0 overflow-hidden': !showSettingsButton && !isHovering,
'w-7 opacity-100': showSettingsButton || isHovering
}"
size="icon"
variant="outline"
{{ t(`model.tags.${tag}`) }}</Badge
>
<Icon icon="lucide:settings-2" class="w-4 h-4" />
</Button>
</template>
<ChatConfig
v-model:temperature="temperature"
v-model:context-length="contextLength"
v-model:max-tokens="maxTokens"
v-model:system-prompt="systemPrompt"
v-model:artifacts="artifacts"
v-model:thinking-budget="thinkingBudget"
v-model:enable-search="enableSearch"
v-model:forced-search="forcedSearch"
v-model:search-strategy="searchStrategy"
v-model:reasoning-effort="reasoningEffort"
v-model:verbosity="verbosity"
:context-length-limit="contextLengthLimit"
:max-tokens-limit="maxTokensLimit"
:model-id="activeModel?.id"
:provider-id="activeModel?.providerId"
:model-type="activeModel?.type"
<Icon icon="lucide:chevron-right" class="w-4 h-4 text-muted-foreground" />
</Button>
</PopoverTrigger>
<PopoverContent align="end" class="w-80 p-0">
<ModelSelect
:type="[ModelType.Chat, ModelType.ImageGeneration]"
@update:model="handleModelUpdate"
/>
</ScrollablePopover>
</div>
</PopoverContent>
</Popover>
<Popover v-model:open="modelSelectOpen">
<PopoverTrigger as-child>
<Button
variant="ghost"
class="flex items-center gap-1.5 h-7 px-2 rounded-md text-xs font-semibold text-muted-foreground hover:bg-muted/60 hover:text-foreground dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
size="sm"
:aria-label="t('model.select.trigger')"
>
<ModelIcon
class="w-4 h-4"
:model-id="activeModel.providerId"
:is-dark="themeStore.isDark"
></ModelIcon>
<span class="text-xs font-semibold truncate max-w-[140px] text-foreground">{{
name
}}</span>
<Badge
v-for="tag in activeModel.tags"
:key="tag"
variant="outline"
class="py-0 px-1 rounded-lg text-[10px]"
>
{{ t(`model.tags.${tag}`) }}</Badge
>
<Icon icon="lucide:chevron-right" class="w-4 h-4 text-muted-foreground" />
</Button>
</PopoverTrigger>
<PopoverContent align="end" class="w-80 p-0">
<ModelSelect
:type="[ModelType.Chat, ModelType.ImageGeneration]"
@update:model="handleModelUpdate"
/>
</PopoverContent>
</Popover>
🤖 Prompt for AI Agents
In src/renderer/src/components/NewThread.vue around lines 19 to 51, the model
selection trigger Button is missing an accessible label for screen readers; add
an aria-label attribute to the Button that uses i18n (e.g.
t('model.select_button') or better t('model.select_button_with_name', { name }))
so it conveys purpose (optionally include the current model name for context),
and then add the corresponding i18n key(s) to the locale files following
existing translations (one key for a generic label and one with a name
placeholder if you include name interpolation).


<ScrollablePopover
v-model:open="settingsPopoverOpen"
align="end"
content-class="w-80"
:enable-scrollable="true"
>
<template #trigger>
<Button
class="h-7 w-7 rounded-md border border-border/60 hover:border-border dark:border-white/10 dark:bg-white/[0.04] dark:text-white/70 dark:hover:border-white/25 dark:hover:bg-white/15 dark:hover:text-white"
size="icon"
variant="outline"
>
<Icon icon="lucide:settings-2" class="w-4 h-4" />
</Button>
</template>
<ChatConfig
v-model:temperature="temperature"
v-model:context-length="contextLength"
v-model:max-tokens="maxTokens"
v-model:system-prompt="systemPrompt"
v-model:artifacts="artifacts"
v-model:thinking-budget="thinkingBudget"
v-model:enable-search="enableSearch"
v-model:forced-search="forcedSearch"
v-model:search-strategy="searchStrategy"
v-model:reasoning-effort="reasoningEffort"
v-model:verbosity="verbosity"
:context-length-limit="contextLengthLimit"
:max-tokens-limit="maxTokensLimit"
:model-id="activeModel?.id"
:provider-id="activeModel?.providerId"
:model-type="activeModel?.type"
/>
</ScrollablePopover>
Comment on lines +53 to +86
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 | 🟠 Major

Add accessible label for the settings trigger button.

The icon-only settings button lacks an accessible label for screen readers.

Apply this diff:

               <Button
                 class="h-7 w-7 rounded-md border border-border/60 hover:border-border dark:border-white/10 dark:bg-white/[0.04] dark:text-white/70 dark:hover:border-white/25 dark:hover:bg-white/15 dark:hover:text-white"
                 size="icon"
                 variant="outline"
+                :aria-label="t('settings.trigger')"
               >

Add the corresponding i18n key. As per coding guidelines

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

🤖 Prompt for AI Agents
In src/renderer/src/components/NewThread.vue around lines 53 to 86, the
icon-only settings trigger Button lacks an accessible label; add an accessible
label by setting the Button's aria-label (or :aria-label binding) to an i18n
string (e.g., $t('newThread.settingsButton')) and update the i18n resource files
with the corresponding key and translations; ensure the aria-label uses the
active locale via the existing i18n setup and keep the label succinct (e.g.,
"Settings") so screen readers can announce the button.

</template>
</ChatInput>
<div class="h-12"></div>
Expand All @@ -118,14 +108,11 @@ import { computed, nextTick, ref, watch, onMounted } from 'vue'
import { UserMessageContent } from '@shared/chat'
import ChatConfig from './ChatConfig.vue'
import { usePresenter } from '@/composables/usePresenter'
import { useEventListener } from '@vueuse/core'
import { useThemeStore } from '@/stores/theme'
import { useLanguageStore } from '@/stores/language'
import { ModelType } from '@shared/model'

const configPresenter = usePresenter('configPresenter')
const themeStore = useThemeStore()
const langStore = useLanguageStore()
// 定义偏好模型的类型
interface PreferredModel {
modelId: string
Expand Down Expand Up @@ -306,17 +293,7 @@ watch(

const modelSelectOpen = ref(false)
const settingsPopoverOpen = ref(false)
const showSettingsButton = ref(false)
const isHovering = ref(false)
const chatInputRef = ref<InstanceType<typeof ChatInput> | null>(null)
// 监听鼠标悬停
const handleMouseEnter = () => {
isHovering.value = true
}

const handleMouseLeave = () => {
isHovering.value = false
}

const handleModelUpdate = (model: MODEL_META, providerId: string) => {
activeModel.value = {
Expand Down Expand Up @@ -405,41 +382,13 @@ watch(
)

onMounted(async () => {
const groupElement = document.querySelector('.new-thread-model-select')
configPresenter.getDefaultSystemPrompt().then((prompt) => {
systemPrompt.value = prompt
})
// 组件激活时初始化一次默认模型
await initActiveModel()
if (groupElement) {
useEventListener(groupElement, 'mouseenter', handleMouseEnter)
useEventListener(groupElement, 'mouseleave', handleMouseLeave)
}
})

const handleSettingsPopoverUpdate = (isOpen: boolean) => {
if (isOpen) {
// 如果打开,立即显示按钮
showSettingsButton.value = true
} else {
// 如果关闭,延迟隐藏按钮,等待动画完成
setTimeout(() => {
showSettingsButton.value = false
}, 300) // 300ms是一个常见的动画持续时间,可以根据实际情况调整
}
}

// 初始化时设置showSettingsButton的值与settingsPopoverOpen一致
watch(
settingsPopoverOpen,
(value) => {
if (value) {
showSettingsButton.value = true
}
},
{ immediate: true }
)

const handleSend = async (content: UserMessageContent) => {
const threadId = await chatStore.createThread(content.text, {
providerId: activeModel.value.providerId,
Expand All @@ -461,14 +410,3 @@ const handleSend = async (content: UserMessageContent) => {
chatStore.sendMessage(content)
}
</script>

<style scoped>
.transition-all {
transition-property: all;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}

.duration-300 {
transition-duration: 300ms;
}
</style>
5 changes: 2 additions & 3 deletions src/renderer/src/components/chat-input/ChatInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -100,13 +100,12 @@
</Tooltip>

<McpToolsList />

<!-- Slot for NewThread model selector -->
<slot name="addon-buttons"></slot>
</div>

<!-- Actions -->
<div class="flex items-center gap-2 flex-wrap">
<!-- NewThread model selector and settings (right-aligned) -->
<slot name="addon-actions"></slot>
<div
v-if="shouldShowContextLength"
:class="[
Expand Down
55 changes: 50 additions & 5 deletions src/renderer/src/components/markdown/MarkdownRenderer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ defineEmits(['copy'])
}

p {
@apply my-0;
@apply my-2;
}

li p {
Expand All @@ -134,12 +134,32 @@ defineEmits(['copy'])
margin-top: 0;
margin-bottom: 0;
}
h1 {
@apply text-2xl font-bold my-3 py-0;
}
h2 {
@apply text-xl font-medium my-3 py-0;
}
h3 {
@apply text-base font-medium my-2 py-0;
}
h4 {
@apply text-sm font-medium my-2 py-0;
}
h5 {
@apply text-sm my-1.5 py-0;
}
h6 {
@apply text-sm my-1.5 py-0;
}

ul,
ol {
@apply my-1.5;
}

hr {
margin-block-start: 0.5em;
margin-block-end: 0.5em;
margin-inline-start: auto;
margin-inline-end: auto;
@apply my-8;
}

/*
Expand All @@ -150,5 +170,30 @@ defineEmits(['copy'])
a .markdown-renderer {
display: inline;
}

.table-node-wrapper {
@apply border border-border rounded-lg py-0 my-0 overflow-hidden shadow-sm;
}

table {
@apply py-0 my-0;
/* @apply bg-card border block rounded-lg my-0 py-0 overflow-hidden; */
border-collapse: collapse;
}

thead,
thead tr,
thead th {
@apply bg-muted;
}

th,
td {
@apply border-b not-last:border-r border-border;
Comment on lines +190 to +192

Choose a reason for hiding this comment

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

P0 Badge Replace unsupported Tailwind variant usage in table cell styles

The new table styling uses @apply border-b not-last:border-r border-border. Tailwind in this repo only declares the default variants plus a custom dark variant in assets/style.css, and there is no plugin defining not-last. During a build Tailwind will error with “The variant not-last does not exist…”, causing the stylesheet compilation to fail. Use a supported approach such as last:border-r-0 or an arbitrary selector ([&:not(:last-child)]:border-r) instead.

Useful? React with 👍 / 👎.

}

tbody tr:last-child td {
@apply border-b-0;
}
}
</style>
27 changes: 18 additions & 9 deletions src/renderer/src/components/message/MessageBlockError.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,22 @@
<Icon icon="lucide:refresh-cw-off"></Icon>
<span>{{ t(block.content || '') }}</span>
</div>
<div
v-else
class="text-xs bg-red-100 text-red-700 rounded-lg border border-red-400 flex flex-col gap-2 px-2 py-2"
>
<div class="flex flex-row gap-2 items-center cursor-pointer">
<Icon icon="lucide:info" class="w-4 h-4 text-red-700" />
<span class="grow">{{ t('common.error.requestFailed') }}</span>
<div v-else class="cursor-default select-none">
<div
class="text-xs text-red-500 flex flex-row items-center group"
@click="isExpanded = !isExpanded"
>
{{ t('common.error.requestFailed')
}}<Icon
class="hidden group-hover:block ml-2 transition-all"
:class="[isExpanded ? ' rotate-90' : '']"
icon="lucide:chevron-right"
></Icon>
</div>
Comment on lines +9 to 20
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 | 🟠 Major

Add accessibility support for the collapsible error header.

The collapsible header should support keyboard navigation and include proper ARIA attributes for screen readers.

Apply this diff to improve accessibility:

     <div
-      class="text-xs text-red-500 flex flex-row items-center group"
+      role="button"
+      tabindex="0"
+      :aria-expanded="isExpanded"
+      aria-controls="error-content"
+      class="text-xs text-red-500 flex flex-row items-center group cursor-pointer"
       @click="isExpanded = !isExpanded"
+      @keydown.enter="isExpanded = !isExpanded"
+      @keydown.space.prevent="isExpanded = !isExpanded"
     >

And add an id to the error content container:

     <div
       v-if="isExpanded"
+      id="error-content"
       class="text-xs max-w-full break-all whitespace-pre-wrap leading-7 text-red-400"
     >

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

🤖 Prompt for AI Agents
In src/renderer/src/components/message/MessageBlockError.vue around lines 9 to
20, the collapsible error header lacks keyboard accessibility and ARIA
attributes; update the header container to be operable via keyboard (add
tabindex="0" and a keydown handler that toggles isExpanded when Enter or Space
is pressed), add role="button", add :aria-expanded="isExpanded" and
aria-controls pointing to the error content id, and ensure the error content
container has a stable id (e.g., error-details-<unique> or a component-local id)
and a corresponding :aria-hidden="!isExpanded"; implement the keydown handler in
the component methods and keep click behavior intact so both mouse and keyboard
toggle the panel.

<div class="prose prose-sm max-w-full break-all whitespace-pre-wrap leading-7">
<div
v-if="isExpanded"
class="text-xs max-w-full break-all whitespace-pre-wrap leading-7 text-red-400"
>
{{ t(block.content || '') }}
</div>
<div v-if="errorExplanation" class="mt-2 text-red-400 font-medium">
Expand All @@ -26,14 +33,16 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import { Icon } from '@iconify/vue'
import { computed } from 'vue'
import { computed, ref } from 'vue'
import { AssistantMessageBlock } from '@shared/chat'
const { t } = useI18n()

const props = defineProps<{
block: AssistantMessageBlock
}>()

const isExpanded = ref(false)

const errorExplanation = computed(() => {
const content = props.block.content || ''

Expand Down
Loading