refactor(web): migrate all icons from FontAwesome to Lucide and remove dead code

Replace all FontAwesome icon usage across 80+ Vue files with lucide-vue-next
components. Remove FontAwesome dependencies (@fortawesome/*) and global
registration from main.ts. Delete unused components (data-table, warning-banner,
session-metadata, bot-sidebar/bot-item in home, message-list, tts-provider-select),
dead utilities (channel-icons.ts, custom-icons.ts), and stale assets (vue.svg).
Update AGENTS.md to reflect the new icon strategy.
This commit is contained in:
Acbox
2026-03-29 17:46:33 +08:00
parent d133a85fe3
commit a2941967df
92 changed files with 407 additions and 1629 deletions
+5 -3
View File
@@ -16,7 +16,7 @@
| Data Fetching | Pinia Colada (`@pinia/colada`) + `@memohai/sdk` |
| Forms | vee-validate + `@vee-validate/zod` + Zod |
| i18n | vue-i18n (en / zh) |
| Icons | FontAwesome (primary: fas/far/fab) + lucide-vue-next (secondary) |
| Icons | lucide-vue-next (primary) + `@memohai/icon` (brand/provider icons) |
| Toast | vue-sonner |
| Tables | @tanstack/vue-table |
| Markdown | markstream-vue + Shiki + Mermaid + KaTeX |
@@ -308,8 +308,9 @@ const form = useForm({
### Icon Usage
- **FontAwesome** (primary): Global `<FontAwesomeIcon :icon="['fas', 'robot']" />`, full `fas`/`far`/`fab` sets + custom search icons registered in `main.ts`
- **Lucide** (secondary): Direct imports `<Sun />`, `<Moon />`, used for theme toggle
- **Lucide** (primary): Direct component imports from `lucide-vue-next`. Example: `import { Plus, Search, Bot } from 'lucide-vue-next'``<Plus class="size-4" />`. Used for all UI icons (actions, navigation, status indicators, etc.).
- **`@memohai/icon`** (brand icons): Workspace package (`packages/icons/`) providing AI provider, search engine, and channel platform SVG icons as Vue components. Example: `import { Openai, Claude } from '@memohai/icon'`.
- **Do NOT use FontAwesome** for new code. Legacy FontAwesome usage remains only in commented-out code blocks. Always use Lucide for UI icons and `@memohai/icon` for brand logos.
### Notification Pattern
@@ -424,6 +425,7 @@ Chat supports two transport modes: **Server-Sent Events (SSE)** and **WebSocket*
- Style with Tailwind utility classes; avoid `<style>` blocks. Follow the design system in `packages/ui/DESIGN.md`.
- **Always use semantic color tokens** (`text-foreground`, `bg-card`, `border-border`, `text-muted-foreground`, `bg-accent`, etc.) instead of raw colors (`gray-*`, `bg-white`, `text-black`). Never introduce hardcoded Tailwind color classes for themed elements — this breaks dark mode consistency.
- Use `@memohai/ui` components for all UI primitives; do not import Reka UI directly.
- Use `lucide-vue-next` for all UI icons. Use `@memohai/icon` for brand/provider logos. **Never use FontAwesome** — do not add `<FontAwesomeIcon>`, do not import from `@fortawesome/*`, do not use inline SVG or base64-encoded SVG in templates.
- Use Pinia Colada (`useQuery`/`useMutation`) for server state; use Pinia stores for client state only.
- API calls must go through `@memohai/sdk`; never call `fetch()` directly.
- All user-facing strings must use i18n keys (`t('key')`) — never hardcode text.
-5
View File
@@ -9,11 +9,6 @@
"start": "vite preview"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^7.0.0",
"@fortawesome/free-brands-svg-icons": "^7.0.0",
"@fortawesome/free-regular-svg-icons": "^7.0.0",
"@fortawesome/free-solid-svg-icons": "^7.0.0",
"@fortawesome/vue-fontawesome": "^3.1.1",
"@memohai/icon": "workspace:*",
"@memohai/sdk": "workspace:*",
"@memohai/ui": "workspace:*",
-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

Before

Width:  |  Height:  |  Size: 496 B

@@ -14,8 +14,7 @@
class="w-full shadow-none! text-muted-foreground mb-4"
variant="outline"
>
<FontAwesomeIcon
:icon="['fas', 'plus']"
<Plus
class="mr-1"
/> {{ $t('provider.addBtn') }}
</Button>
@@ -163,6 +162,7 @@ import { useMutation, useQueryCache } from '@pinia/colada'
import { postProviders, postProvidersByIdImportModels } from '@memohai/sdk'
import type { ProvidersCreateRequest } from '@memohai/sdk'
import { useI18n } from 'vue-i18n'
import { Plus } from 'lucide-vue-next'
import FormDialogShell from '@/components/form-dialog-shell/index.vue'
import { useDialogMutation } from '@/composables/useDialogMutation'
import SearchableSelectPopover from '@/components/searchable-select-popover/index.vue'
@@ -5,28 +5,22 @@
alt="logo.png"
class="w-6.5"
>
<img
<LoaderCircle
v-if="isLoading"
src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSIjOEI1Q0Y2IiBkPSJNMTIsMUExMSwxMSwwLDEsMCwyMywxMiwxMSwxMSwwLDAsMCwxMiwxWm0wLDE5YTgsOCwwLDEsMSw4LThBOCw4LDAsMCwxLDEyLDIwWiIgb3BhY2l0eT0iMC4yNSIvPjxwYXRoIGZpbGw9IiM4QjVDRjYiIGQ9Ik0xMC4xNCwxLjE2YTExLDExLDAsMCwwLTksOC45MkExLjU5LDEuNTksMCwwLDAsMi40NiwxMiwxLjUyLDEuNTIsMCwwLDAsNC4xMSwxMC43YTgsOCwwLDAsMSw2LjY2LTYuNjFBMS40MiwxLjQyLDAsMCwwLDEyLDIuNjloMEExLjU3LDEuNTcsMCwwLDAsMTAuMTQsMS4xNloiPjxhbmltYXRlVHJhbnNmb3JtIGF0dHJpYnV0ZU5hbWU9InRyYW5zZm9ybSIgZHVyPSIwLjc1cyIgcmVwZWF0Q291bnQ9ImluZGVmaW5pdGUiIHR5cGU9InJvdGF0ZSIgdmFsdWVzPSIwIDEyIDEyOzM2MCAxMiAxMiIvPjwvcGF0aD48L3N2Zz4="
alt="loading"
width="13"
class="absolute bottom-0 right-0 bg-card p-0.5 rounded-full"
>
<img
class="absolute bottom-0 right-0 bg-card p-0.5 rounded-full size-[13px] text-primary animate-spin"
/>
<CircleAlert
v-if="error"
src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iOSIgaGVpZ2h0PSI5IiB2aWV3Qm94PSIwIDAgOSA5IiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgo8bWFzayBpZD0icGF0aC0xLWluc2lkZS0xXzE0OF80NDgiIGZpbGw9IndoaXRlIj4KPHBhdGggZD0iTTQuNSA4LjI1QzIuNDI4OTMgOC4yNSAwLjc1IDYuNTcxMDUgMC43NSA0LjVDMC43NSAyLjQyODkzIDIuNDI4OTMgMC43NSA0LjUgMC43NUM2LjU3MTA1IDAuNzUgOC4yNSAyLjQyODkzIDguMjUgNC41QzguMjUgNi41NzEwNSA2LjU3MTA1IDguMjUgNC41IDguMjVaTTQuNSA3LjVDNi4xNTY4NiA3LjUgNy41IDYuMTU2ODYgNy41IDQuNUM3LjUgMi44NDMxNCA2LjE1Njg2IDEuNSA0LjUgMS41QzIuODQzMTQgMS41IDEuNSAyLjg0MzE0IDEuNSA0LjVDMS41IDYuMTU2ODYgMi44NDMxNCA3LjUgNC41IDcuNVpNNC4xMjUgNS42MjVINC44NzVWNi4zNzVINC4xMjVWNS42MjVaTTQuMTI1IDIuNjI1SDQuODc1VjQuODc1SDQuMTI1VjIuNjI1WiIvPgo8L21hc2s+CjxwYXRoIGQ9Ik00LjEyNSA1LjYyNVY0Ljg3NUgzLjM3NVY1LjYyNUg0LjEyNVpNNC44NzUgNS42MjVINS42MjVWNC44NzVINC44NzVWNS42MjVaTTQuODc1IDYuMzc1VjcuMTI1SDUuNjI1VjYuMzc1SDQuODc1Wk00LjEyNSA2LjM3NUgzLjM3NVY3LjEyNUg0LjEyNVY2LjM3NVpNNC4xMjUgMi42MjVWMS44NzVIMy4zNzVWMi42MjVINC4xMjVaTTQuODc1IDIuNjI1SDUuNjI1VjEuODc1SDQuODc1VjIuNjI1Wk00Ljg3NSA0Ljg3NVY1LjYyNUg1LjYyNVY0Ljg3NUg0Ljg3NVpNNC4xMjUgNC44NzVIMy4zNzVWNS42MjVINC4xMjVWNC44NzVaTTQuNSA4LjI1VjcuNUMyLjg0MzE1IDcuNSAxLjUgNi4xNTY4NCAxLjUgNC41SDAuNzVIMEMwIDYuOTg1MjYgMi4wMTQ3MiA5IDQuNSA5VjguMjVaTTAuNzUgNC41SDEuNUMxLjUgMi44NDMxNCAyLjg0MzE0IDEuNSA0LjUgMS41VjAuNzVWMEMyLjAxNDcyIDAgMCAyLjAxNDcyIDAgNC41SDAuNzVaTTQuNSAwLjc1VjEuNUM2LjE1Njg0IDEuNSA3LjUgMi44NDMxNSA3LjUgNC41SDguMjVIOUM5IDIuMDE0NzIgNi45ODUyNiAwIDQuNSAwVjAuNzVaTTguMjUgNC41SDcuNUM3LjUgNi4xNTY4NCA2LjE1Njg0IDcuNSA0LjUgNy41VjguMjVWOUM2Ljk4NTI2IDkgOSA2Ljk4NTI2IDkgNC41SDguMjVaTTQuNSA3LjVWOC4yNUM2LjU3MTA4IDguMjUgOC4yNSA2LjU3MTA4IDguMjUgNC41SDcuNUg2Ljc1QzYuNzUgNS43NDI2NSA1Ljc0MjY1IDYuNzUgNC41IDYuNzVWNy41Wk03LjUgNC41SDguMjVDOC4yNSAyLjQyODkzIDYuNTcxMDggMC43NSA0LjUgMC43NVYxLjVWMi4yNUM1Ljc0MjY1IDIuMjUgNi43NSAzLjI1NzM2IDYuNzUgNC41SDcuNVpNNC41IDEuNVYwLjc1QzIuNDI4OTMgMC43NSAwLjc1IDIuNDI4OTMgMC43NSA0LjVIMS41SDIuMjVDMi4yNSAzLjI1NzM2IDMuMjU3MzYgMi4yNSA0LjUgMi4yNVYxLjVaTTEuNSA0LjVIMC43NUMwLjc1IDYuNTcxMDggMi40Mjg5MyA4LjI1IDQuNSA4LjI1VjcuNVY2Ljc1QzMuMjU3MzYgNi43NSAyLjI1IDUuNzQyNjUgMi4yNSA0LjVIMS41Wk00LjEyNSA1LjYyNVY2LjM3NUg0Ljg3NVY1LjYyNVY0Ljg3NUg0LjEyNVY1LjYyNVpNNC44NzUgNS42MjVINC4xMjVWNi4zNzVINC44NzVINS42MjVWNS42MjVINC44NzVaTTQuODc1IDYuMzc1VjUuNjI1SDQuMTI1VjYuMzc1VjcuMTI1SDQuODc1VjYuMzc1Wk00LjEyNSA2LjM3NUg0Ljg3NVY1LjYyNUg0LjEyNUgzLjM3NVY2LjM3NUg0LjEyNVpNNC4xMjUgMi42MjVWMy4zNzVINC44NzVWMi42MjVWMS44NzVINC4xMjVWMi42MjVaTTQuODc1IDIuNjI1SDQuMTI1VjQuODc1SDQuODc1SDUuNjI1VjIuNjI1SDQuODc1Wk00Ljg3NSA0Ljg3NVY0LjEyNUg0LjEyNVY0Ljg3NVY1LjYyNUg0Ljg3NVY0Ljg3NVpNNC4xMjUgNC44NzVINC44NzVWMi42MjVINC4xMjVIMy4zNzVWNC44NzVINC4xMjVaIiBmaWxsPSIjNzM3MzczIiBtYXNrPSJ1cmwoI3BhdGgtMS1pbnNpZGUtMV8xNDhfNDQ4KSIvPgo8L3N2Zz4K"
alt="error"
width="13"
class="absolute bottom-0 right-0 bg-card p-0.5 rounded-full"
>
class="absolute bottom-0 right-0 bg-card p-0.5 rounded-full size-[13px] text-muted-foreground"
/>
</section>
</template>
<script setup lang="ts">
import { CircleAlert, LoaderCircle } from 'lucide-vue-next'
withDefaults(defineProps < { isLoading?: boolean ,error?:boolean} > (), {
withDefaults(defineProps<{ isLoading?: boolean; error?: boolean }>(), {
isLoading: true,
error:false
error: false,
})
</script>
@@ -1,77 +0,0 @@
<script setup lang="ts" generic="TData, TValue">
import type { ColumnDef } from '@tanstack/vue-table'
import {
FlexRender,
getCoreRowModel,
useVueTable,
} from '@tanstack/vue-table'
import {
Table,
TableBody,
TableCell,
TableEmpty,
TableHead,
TableHeader,
TableRow,
} from '@memohai/ui'
const props = defineProps<{
columns: ColumnDef<TData, TValue>[]
data: TData[]
}>()
const table = useVueTable({
get data() { return props.data },
get columns() { return props.columns },
getCoreRowModel: getCoreRowModel(),
})
</script>
<template>
<div class="border rounded-md">
<Table>
<TableHeader>
<TableRow
v-for="headerGroup in table.getHeaderGroups()"
:key="headerGroup.id"
>
<TableHead
v-for="header in headerGroup.headers"
:key="header.id"
>
<FlexRender
v-if="!header.isPlaceholder"
:render="header.column.columnDef.header"
:props="header.getContext()"
/>
</TableHead>
</TableRow>
</TableHeader>
<TableBody class="[&_td]:py-4!">
<template v-if="table.getRowModel().rows?.length">
<TableRow
v-for="row in table.getRowModel().rows"
:key="row.id"
:data-state="row.getIsSelected() ? 'selected' : undefined"
>
<TableCell
v-for="cell in row.getVisibleCells()"
:key="cell.id"
>
<FlexRender
:render="cell.column.columnDef.cell"
:props="cell.getContext()"
/>
</TableCell>
</TableRow>
</template>
<template v-else>
<TableEmpty :colspan="columns.length">
No results.
</TableEmpty>
</template>
</TableBody>
</Table>
</div>
</template>
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import { LoaderCircle, FolderOpen, Folder, File, Download, SquarePen, Trash2 } from 'lucide-vue-next'
import {
ContextMenu,
ContextMenuContent,
@@ -52,8 +52,7 @@ function handleClick(entry: HandlersFsFileInfo) {
v-if="loading"
class="flex items-center justify-center py-16 text-muted-foreground"
>
<FontAwesomeIcon
:icon="['fas', 'spinner']"
<LoaderCircle
class="mr-2 size-4 animate-spin"
/>
{{ t('common.loading') }}
@@ -63,8 +62,7 @@ function handleClick(entry: HandlersFsFileInfo) {
v-else-if="sortedEntries.length === 0"
class="flex flex-col items-center justify-center py-16 text-muted-foreground"
>
<FontAwesomeIcon
:icon="['fas', 'folder-open']"
<FolderOpen
class="mb-2 size-8 opacity-40"
/>
<span>{{ t('bots.files.empty') }}</span>
@@ -95,8 +93,8 @@ function handleClick(entry: HandlersFsFileInfo) {
@click="handleClick(entry)"
>
<div class="flex flex-1 items-center gap-2 min-w-0">
<FontAwesomeIcon
:icon="entry.isDir ? ['fas', 'folder'] : ['fas', 'file']"
<component
:is="entry.isDir ? Folder : File"
:class="entry.isDir ? 'text-blue-500' : 'text-muted-foreground'"
class="size-4 shrink-0"
/>
@@ -115,15 +113,13 @@ function handleClick(entry: HandlersFsFileInfo) {
v-if="!entry.isDir"
@select="emit('download', entry)"
>
<FontAwesomeIcon
:icon="['fas', 'download']"
<Download
class="mr-2 size-3.5"
/>
{{ t('bots.files.download') }}
</ContextMenuItem>
<ContextMenuItem @select="emit('rename', entry)">
<FontAwesomeIcon
:icon="['fas', 'pen']"
<SquarePen
class="mr-2 size-3.5"
/>
{{ t('bots.files.rename') }}
@@ -133,8 +129,7 @@ function handleClick(entry: HandlersFsFileInfo) {
class="text-destructive focus:text-destructive"
@select="emit('delete', entry)"
>
<FontAwesomeIcon
:icon="['fas', 'trash']"
<Trash2
class="mr-2 size-3.5"
/>
{{ t('bots.files.delete') }}
@@ -13,7 +13,7 @@
variant="outline"
class="flex items-center gap-2"
>
<FontAwesomeIcon :icon="['fas', 'file-import']" />
<FileInput />
{{ $t('models.importModels') }}
</Button>
</template>
@@ -33,6 +33,7 @@ import { useI18n } from 'vue-i18n'
import { useMutation, useQueryCache } from '@pinia/colada'
import { postProvidersByIdImportModels } from '@memohai/sdk'
import { toast } from 'vue-sonner'
import { FileInput } from 'lucide-vue-next'
import { Button } from '@memohai/ui'
import FormDialogShell from '@/components/form-dialog-shell/index.vue'
import { useDialogMutation } from '@/composables/useDialogMutation'
@@ -1,4 +1,5 @@
<script setup lang="ts">
import { X, Plus } from 'lucide-vue-next'
import { Button, Input } from '@memohai/ui'
export interface KeyValuePair {
@@ -67,7 +68,7 @@ function removeRow(index: number) {
class="shrink-0 size-8 text-muted-foreground hover:text-destructive"
@click="removeRow(index)"
>
<FontAwesomeIcon :icon="['fas', 'xmark']" />
<X />
</Button>
</div>
<Button
@@ -78,8 +79,7 @@ function removeRow(index: number) {
class="w-fit"
@click="addRow"
>
<FontAwesomeIcon
:icon="['fas', 'plus']"
<Plus
class="mr-1.5"
/>
{{ $t('common.add') }}
@@ -24,8 +24,7 @@
</section>
<div class="fixed right-4 top-0 h-12 z-1000 md:hidden flex items-center">
<FontAwesomeIcon
:icon="['fas', 'bars']"
<Menu
class="cursor-pointer p-2"
@click="mobileOpen = !mobileOpen"
/>
@@ -62,6 +61,7 @@
</template>
<script setup lang="ts">
import { Menu } from 'lucide-vue-next'
import { ref } from 'vue'
import {
Sheet,
@@ -1,93 +0,0 @@
/**
* Custom FontAwesome icon definitions for search providers
* that don't have icons in the FA free icon packs.
*
* Sources:
* - tavily, jina, exa, bocha, serper: brand SVGs provided externally
* - duckduckgo, searxng, sogou: Simple Icons (https://simpleicons.org)
*
* Each icon is registered with the 'fac' (custom) prefix and can be used
* as ['fac', 'icon-name'] in FontAwesomeIcon components.
*/
import type { IconDefinition } from '@fortawesome/fontawesome-svg-core'
function defineCustomIcon(
name: string,
width: number,
height: number,
pathData: string,
): IconDefinition {
return {
prefix: 'fac' as IconDefinition['prefix'],
iconName: name as IconDefinition['iconName'],
icon: [width, height, [], '', pathData],
}
}
export const facTavily = defineCustomIcon(
'tavily',
24,
24,
'M8.033 14.273a1.612 1.612 0 011.139.47l.04.042.044.043a1.61 1.61 0 010 2.277l-3.073 3.073.816.816c.6.6.303 1.627-.525 1.814l-5.159 1.165a1.07 1.07 0 01-.897-.2l-.102-.09a1.07 1.07 0 01-.289-1l1.164-5.158A1.079 1.079 0 013.006 17l.816.817 3.074-3.074a1.612 1.612 0 011.137-.47zM17.042 13.246c0-.85.935-1.366 1.653-.912l4.47 2.824c.336.212.503.562.503.911 0 .35-.167.7-.501.913l-4.472 2.824a1.079 1.079 0 01-1.654-.912v-1.155h-7.027c.37-.4.605-.902.677-1.438l.022-.232a2.65 2.65 0 00-.492-1.669h6.821v-1.154zM8.188 0c.35 0 .7.168.913.503l2.823 4.47a1.079 1.079 0 01-.911 1.655H9.857v6.692h-1.67a2.633 2.633 0 00-1.668.48V6.629H5.365c-.849 0-1.366-.936-.912-1.654L7.276.503A1.072 1.072 0 018.188 0z',
)
export const facJina = defineCustomIcon(
'jina',
24,
24,
'M6.608 21.416a4.608 4.608 0 100-9.217 4.608 4.608 0 000 9.217zM20.894 2.015c.614 0 1.106.492 1.106 1.106v9.002c0 5.13-4.148 9.309-9.217 9.37v-9.355l-.03-9.032c0-.614.491-1.106 1.106-1.106h7.158l-.123.015z',
)
export const facExa = defineCustomIcon(
'exa',
24,
24,
'M3 0h19v1.791L13.892 12 22 22.209V24H3V0zm9.62 10.348l6.589-8.557H6.03l6.59 8.557zM5.138 3.935v7.17h5.52l-5.52-7.17zm5.52 8.96h-5.52v7.17l5.52-7.17zM6.03 22.21l6.59-8.557 6.589 8.557H6.03z',
)
export const facBocha = defineCustomIcon(
'bocha',
463,
395,
'M52.6531 54.652C91.561 35.683 138.473 51.843 157.46 90.756L197.998 173.838C207.483 193.277 199.421 216.728 179.991 226.217C179.983 226.221 179.975 226.225 179.967 226.229C160.513 235.713 137.057 227.633 127.563 208.177L52.6531 54.652Z M12 133.819C42.6115 103.22 92.214 103.22 122.826 133.819L190.618 201.583C205.907 216.865 205.918 241.653 190.642 256.949C190.634 256.957 190.626 256.965 190.618 256.973C175.313 272.272 150.511 272.272 135.205 256.973L12 133.819Z M292.854 68.333C380.336 68.333 451.253 139.222 451.253 226.667C451.253 269.36 429.304 312.74 401.821 341.217L390.442 327.576C370.921 304.18 363.989 272.751 371.853 243.309C373.277 237.973 373.99 233.071 373.99 228.602C373.99 183.499 337.411 146.935 292.289 146.935C247.168 146.935 210.589 183.499 210.589 228.602C210.589 273.705 247.168 310.269 292.289 310.269C300.737 310.269 308.884 308.987 316.549 306.608C339.517 299.479 364.544 305.399 381.889 322.065L401.821 341.217C373.52 367.628 334.626 385 292.854 385C205.373 385 134.456 314.112 134.456 226.667C134.456 139.222 205.373 68.333 292.854 68.333Z M134.456 10C177.736 10 212.821 45.102 212.821 88.401V144.932C212.821 188.232 177.736 223.333 134.456 223.333V10Z',
)
export const facDuckduckgo = defineCustomIcon(
'duckduckgo',
24,
24,
'M12 0C5.37 0 0 5.37 0 12s5.37 12 12 12s12-5.37 12-12S18.63 0 12 0m0 .984C18.083.984 23.016 5.916 23.016 12S18.084 23.016 12 23.016S.984 18.084.984 12S5.916.984 12 .984m0 .938C6.434 1.922 1.922 6.434 1.922 12c0 4.437 2.867 8.205 6.85 9.55c-.237-.82-.776-2.753-1.6-6.052c-1.184-4.741-2.064-8.606 2.379-9.813c.047-.011.064-.064.03-.093c-.514-.467-1.382-.548-2.233-.38a.06.06 0 0 1-.07-.058c0-.011 0-.023.011-.035c.205-.286.572-.507.822-.64a1.8 1.8 0 0 0-.607-.335c-.059-.022-.059-.12-.006-.144q.008-.01.024-.012c1.749-.233 3.586.292 4.49 1.448a.1.1 0 0 0 .035.023c2.968.635 3.509 4.837 3.328 5.998a9.6 9.6 0 0 0 2.346-.576c.746-.286 1.008-.222 1.101-.053c.1.193-.018.513-.28.81c-.496.567-1.393 1.01-2.974 1.137c-.546.044-1.029.024-1.445.006c-.789-.035-1.339-.059-1.633.39c-.192.298-.041.998 1.487 1.22c1.09.157 2.078.047 2.798-.034c.643-.07 1.073-.118 1.172.069c.21.402-.996 1.207-3.066 1.224q-.238-.002-.467-.011c-1.283-.065-2.227-.414-2.816-.735a.1.1 0 0 1-.035-.017c-.105-.059-.31.045-.188.267c.07.134.444.478 1.004.776c-.058.466.087 1.184.338 2l.088-.016q.063-.015.134-.025c.507-.082.775.012.926.175c.717-.536 1.913-1.294 2.03-1.154c.583.694.66 2.332.53 2.99q-.006.018-.04.035c-.274.117-1.783-.296-1.783-.511c-.059-1.075-.26-1.173-.493-1.225h-.156a.1.1 0 0 1 .018.03l.052.12c.093.257.24 1.063.13 1.26c-.112.199-.835.297-1.284.303c-.443.006-.543-.158-.637-.408c-.07-.204-.103-.675-.103-.95a1 1 0 0 1 .012-.216c-.134.058-.333.193-.397.281c-.017.262-.017.682.123 1.149c.07.221-1.518 1.164-1.74.99c-.227-.181-.634-1.952-.459-2.67c-.187.017-.338.075-.42.191c-.367.508.093 2.933.582 3.248c.257.169 1.54-.553 2.176-1.095c.105.145.305.158.553.158c.326-.012.782-.06 1.103-.158c.192.45.423.972.613 1.388c4.47-1.032 7.803-5.037 7.803-9.82c0-5.566-4.512-10.078-10.078-10.078m1.791 5.646c-.42 0-.678.146-.795.332c-.023.047.047.094.094.07c.14-.075.357-.161.701-.156c.328.006.516.09.67.159l.023.01c.041.017.088-.03.059-.065c-.134-.18-.332-.35-.752-.35m-5.078.198a1.2 1.2 0 0 0-.522.082c-.454.169-.67.526-.67.76c0 .051.112.057.141.011c.081-.123.21-.31.617-.478c.408-.17.73-.146.951-.094c.047.012.083-.041.041-.07a1 1 0 0 0-.558-.211m5.434 1.423a.65.65 0 0 0-.655.647a.652.652 0 0 0 1.307 0a.646.646 0 0 0-.652-.647m.283.262h.008a.17.17 0 0 1 .17.17c0 .093-.077.17-.17.17a.17.17 0 0 1-.17-.17c0-.09.072-.165.162-.17m-5.358.076a.75.75 0 0 0-.758.758c0 .42.338.758.758.758s.758-.337.758-.758a.756.756 0 0 0-.758-.758m.328.303h.01a.199.199 0 1 1 0 .397a.195.195 0 0 1-.197-.198c0-.107.082-.194.187-.199',
)
export const facSearxng = defineCustomIcon(
'searxng',
24,
24,
'm13.716 17.261l6.873 6.582L24 20.282l-6.824-6.536a9.1 9.1 0 0 0 1.143-4.43c0-5.055-4.105-9.159-9.16-9.159S0 4.261 0 9.316s4.104 9.159 9.159 9.159a9.1 9.1 0 0 0 4.557-1.214M9.159 2.773a6.546 6.546 0 0 1 6.543 6.543a6.545 6.545 0 0 1-6.543 6.542a6.545 6.545 0 0 1-6.542-6.542a6.545 6.545 0 0 1 6.542-6.543M7.26 5.713a4.065 4.065 0 0 1 4.744.747a4.06 4.06 0 0 1 .707 4.749l1.157.611a5.38 5.38 0 0 0-.935-6.282a5.38 5.38 0 0 0-6.274-.987z',
)
export const facSogou = defineCustomIcon(
'sogou',
24,
24,
'M16.801 22.74L17.79 24c1.561-.676 2.926-1.62 4.051-2.851l-.946-1.318a10.3 10.3 0 0 1-4.08 2.909zM12 22.199c-5.775 0-10.455-4.619-10.455-10.35C1.545 6.15 6.225 1.53 12 1.53s10.456 4.65 10.456 10.35c0 2.55-.946 4.891-2.507 6.69l.945 1.261C22.801 17.729 24 14.939 24 11.88C24 5.295 18.63 0 12 0S0 5.311 0 11.85c0 6.57 5.37 11.88 12 11.88c1.71 0 3.33-.346 4.801-.99l-.961-1.26c-1.2.45-2.49.719-3.84.719m6-9.553c-2.25-1.86-5.34-2.101-7.801-3.556c-.75-.479-.148-1.395.602-1.425c2.699-.45 5.369.63 7.889 1.5l.151-2.655c-3.151-1.14-6.57-1.875-9.901-1.35c-1.2.3-2.4.675-3.254 1.56c-1.171 1.2-.961 3.36.389 4.32c2.236 1.755 5.176 2.011 7.621 3.36c.96.39.555 1.68-.391 1.77c-2.925.555-5.805-.721-8.325-2.1c-.03 1.02-.06 2.01-.06 3c3.195 1.409 6.75 2.069 10.2 1.529c1.17-.225 2.37-.6 3.225-1.454c1.229-1.2 1.111-3.511-.33-4.5H18z',
)
export const facSerper = defineCustomIcon(
'serper',
70,
70,
'M35 0A35 35 0 1 1 35 70A35 35 0 1 1 35 0Z M35 6A29 29 0 1 0 35 64A29 29 0 1 0 35 6Z M35.699 17.526c1.8718 0 3.6377.3174 5.2979.9521 1.6764.6348 3.1412 1.5137 4.3945 2.6367 1.2533 1.1231 2.2054 2.4252 2.8564 3.9063.1465.3255.2198.6592.2198 1.001 0 .6673-.2442 1.2451-.7324 1.7334-.4721.472-1.0417.708-1.709.708-.4395 0-.8789-.1384-1.3184-.4151-.4394-.2929-.7487-.6429-.9277-1.0498-.6185-1.3997-1.652-2.5146-3.1006-3.3447-1.4323-.8301-3.0925-1.2451-4.9805-1.2451-.9277 0-1.9124.1139-2.9541.3418-1.0254.2278-1.9938.5778-2.9053 1.0498-.9114.4557-1.652 1.0335-2.2216 1.7334-.5697.6998-.8545 1.5137-.8545 2.4414 0 .7487.2441 1.3997.7324 1.9531.4883.5371 1.0335.9603 1.6357 1.2695 1.4161.6999 2.9786 1.1882 4.6875 1.4649 1.7253.2767 3.4668.5452 5.2246.8057 1.7579.2604 3.3936.7161 4.9073 1.3671.9603.4069 1.8799.9685 2.7588 1.6846.8789.7162 1.595 1.5869 2.1484 2.6123s.8301 2.2298.8301 3.6133c0 1.7904-.4313 3.361-1.294 4.7119-.8626 1.3509-1.9938 2.474-3.3935 3.3691-1.3998.8952-2.9216 1.5707-4.5655 2.0264-1.6276.4395-3.2226.6592-4.7851.6592-2.1159 0-4.1016-.3337-5.957-1.001-1.8555-.6836-3.475-1.6439-4.8584-2.8808-1.3835-1.2533-2.4414-2.7181-3.1739-4.3946-.1465-.3255-.2197-.651-.2197-.9765 0-.6674.236-1.237.708-1.709.4883-.4883 1.0661-.7324 1.7334-.7324.4557 0 .9033.1464 1.3428.4394.4394.2767.7405.6266.9033 1.0498.7162 1.6439 1.9287 2.946 3.6377 3.9063 1.709.944 3.6702 1.416 5.8838 1.416 1.3835 0 2.7751-.2035 4.1748-.6104 1.3997-.4231 2.5716-1.066 3.5156-1.9287.9603-.8789 1.4404-1.9938 1.4404-3.3447 0-.8626-.2929-1.5706-.8789-2.124-.5696-.5697-1.1962-1.001-1.8798-1.294-1.5463-.6673-3.1983-1.123-4.9561-1.3672-1.7578-.2604-3.5075-.5289-5.249-.8056-1.7416-.293-3.3692-.822-4.8828-1.5869-1.2858-.6511-2.4577-1.5951-3.5157-2.8321-1.0416-1.2532-1.5625-2.8401-1.5625-4.7607 0-1.6927.4069-3.182 1.2207-4.4678.8301-1.3021 1.9206-2.3926 3.2715-3.2715 1.3672-.8952 2.8646-1.5706 4.4922-2.0263 1.6276-.4558 3.2471-.6836 4.8584-.6836z',
)
export const customSearchIcons = [
facTavily,
facJina,
facExa,
facBocha,
facDuckduckgo,
facSearxng,
facSogou,
facSerper,
]
@@ -8,15 +8,15 @@
v-if="iconComponent"
:size="iconSize"
/>
<FontAwesomeIcon
<Globe
v-else
:icon="['fas', 'globe']"
:class="iconSizeClass"
/>
</span>
</template>
<script setup lang="ts">
import { Globe } from 'lucide-vue-next'
import { computed, type Component } from 'vue'
import {
Brave,
@@ -18,8 +18,7 @@
<span class="truncate">
{{ displayLabel || placeholder }}
</span>
<FontAwesomeIcon
:icon="['fas', 'magnifying-glass']"
<Search
class="ml-2 size-3.5 shrink-0 text-muted-foreground"
/>
</Button>
@@ -30,8 +29,7 @@
align="start"
>
<div class="flex items-center border-b px-3">
<FontAwesomeIcon
:icon="['fas', 'magnifying-glass']"
<Search
class="mr-2 size-3.5 shrink-0 text-muted-foreground"
/>
<input
@@ -80,9 +78,8 @@
:class="{ 'bg-accent': selected === option.value }"
@click="selectOption(option.value)"
>
<FontAwesomeIcon
<Check
v-if="selected === option.value"
:icon="['fas', 'check']"
class="size-3.5"
/>
<span
@@ -118,6 +115,7 @@
</template>
<script setup lang="ts">
import { Search, Check } from 'lucide-vue-next'
import {
Popover,
PopoverTrigger,
@@ -6,8 +6,7 @@
class="h-[53px] flex items-center gap-2.5 px-3.5 w-full border-b border-border text-foreground hover:bg-accent/50 transition-colors group-data-[collapsible=icon]:justify-center group-data-[collapsible=icon]:px-0"
@click="router.push(backToChatRoute)"
>
<FontAwesomeIcon
:icon="['fas', 'chevron-left']"
<ChevronLeft
class="size-3 shrink-0"
/>
<span class="text-xs font-semibold group-data-[collapsible=icon]:hidden">
@@ -31,8 +30,8 @@
class="h-9 gap-2 relative before:absolute before:w-0.5 before:top-1.5 before:bottom-1.5 before:left-0 before:rounded-full data-[active=true]:before:bg-[#8B56E3]"
@click="router.push({ name: item.name })"
>
<FontAwesomeIcon
:icon="item.icon"
<component
:is="item.icon"
class="size-3.5 ml-1.5"
/>
<span class="text-xs font-medium">{{ item.title }}</span>
@@ -47,10 +46,11 @@
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { computed, type Component } from 'vue'
import { storeToRefs } from 'pinia'
import { useRouter, useRoute } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { ChevronLeft, Bot, Boxes, Globe, Brain, Volume2, Mail, AppWindow, ChartLine, Settings } from 'lucide-vue-next'
import { useChatSelectionStore } from '@/store/chat-selection'
import {
Sidebar,
@@ -89,51 +89,51 @@ function isItemActive(name: string): boolean {
return route.name === name
}
const navItems = computed(() => [
const navItems = computed<{ title: string; name: string; icon: Component }[]>(() => [
{
title: t('sidebar.bots'),
name: 'bots',
icon: ['fas', 'robot'],
icon: Bot,
},
{
title: t('sidebar.providers'),
name: 'providers',
icon: ['fas', 'cubes'],
icon: Boxes,
},
{
title: t('sidebar.webSearch'),
name: 'web-search',
icon: ['fas', 'globe'],
icon: Globe,
},
{
title: t('sidebar.memory'),
name: 'memory',
icon: ['fas', 'brain'],
icon: Brain,
},
{
title: t('sidebar.speech'),
name: 'speech',
icon: ['fas', 'volume-high'],
icon: Volume2,
},
{
title: t('sidebar.email'),
name: 'email',
icon: ['fas', 'envelope'],
icon: Mail,
},
{
title: t('sidebar.browser'),
name: 'browser',
icon: ['fas', 'window-maximize'],
icon: AppWindow,
},
{
title: t('sidebar.usage'),
name: 'usage',
icon: ['fas', 'chart-line'],
icon: ChartLine,
},
{
title: t('sidebar.profile'),
name: 'profile',
icon: ['fas', 'gear'],
icon: Settings,
},
])
</script>
+4 -6
View File
@@ -41,8 +41,7 @@
<span
class="shrink-0 size-6 flex items-center justify-center rounded text-muted-foreground opacity-0 group-hover/bot:opacity-100 hover:text-foreground hover:bg-accent transition-opacity"
>
<FontAwesomeIcon
:icon="['fas', 'ellipsis']"
<Ellipsis
class="size-3"
/>
</span>
@@ -53,15 +52,13 @@
@click.stop
>
<DropdownMenuItem @click.stop="handleTogglePin">
<FontAwesomeIcon
:icon="['fas', 'thumbtack']"
<Pin
class="size-3 mr-2"
/>
{{ pinned ? $t('common.unpin') : $t('common.pin') }}
</DropdownMenuItem>
<DropdownMenuItem @click.stop="handleDetails">
<FontAwesomeIcon
:icon="['fas', 'gear']"
<Settings
class="size-3 mr-2"
/>
{{ $t('common.details') }}
@@ -80,6 +77,7 @@ import type { BotsBot } from '@memohai/sdk'
import { useChatStore } from '@/store/chat-list'
import { useAvatarInitials } from '@/composables/useAvatarInitials'
import { usePinnedBots } from '@/composables/usePinnedBots'
import { Ellipsis, Pin, Settings } from 'lucide-vue-next'
import {
SidebarMenuButton,
DropdownMenu,
+4 -6
View File
@@ -16,8 +16,7 @@
:aria-label="t('bots.createBot')"
@click="router.push({ name: 'bots' })"
>
<FontAwesomeIcon
:icon="['fas', 'plus']"
<Plus
class="size-3.5"
/>
</Button>
@@ -40,8 +39,7 @@
v-if="isLoading"
class="flex justify-center py-4"
>
<FontAwesomeIcon
:icon="['fas', 'spinner']"
<LoaderCircle
class="size-4 animate-spin text-muted-foreground"
/>
</div>
@@ -87,8 +85,7 @@
:is-active="isSettingsActive"
@click="router.push('/settings')"
>
<FontAwesomeIcon
:icon="['fas', 'gear']"
<Settings
class="size-3.5"
/>
<span class="text-xs font-medium">{{ t('sidebar.settings') }}</span>
@@ -121,6 +118,7 @@ import {
SidebarMenuButton,
SidebarMenuItem,
} from '@memohai/ui'
import { Plus, LoaderCircle, Settings } from 'lucide-vue-next'
import BotItem from './bot-item.vue'
import { usePinnedBots } from '@/composables/usePinnedBots'
@@ -1,5 +0,0 @@
<template>
<div class="rounded-md border border-yellow-300/50 bg-yellow-50/70 p-3 text-xs text-yellow-800 dark:border-yellow-800/50 dark:bg-yellow-900/10 dark:text-yellow-200">
<slot />
</div>
</template>
-21
View File
@@ -13,28 +13,7 @@ import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import 'markstream-vue/index.css'
import 'katex/dist/katex.min.css'
// Font Awesome
import { library } from '@fortawesome/fontawesome-svg-core'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import {
fas
} from '@fortawesome/free-solid-svg-icons'
import {
far
} from '@fortawesome/free-regular-svg-icons'
import { fab } from '@fortawesome/free-brands-svg-icons'
import { customSearchIcons } from './components/search-provider-logo/custom-icons'
library.add(
far,
fab,
fas,
...customSearchIcons,
)
createApp(App)
.component('FontAwesomeIcon', FontAwesomeIcon)
.use(createPinia().use(piniaPluginPersistedstate))
.use(PiniaColada)
.use(router)
@@ -77,8 +77,7 @@
size="sm"
@click="openAddDialog"
>
<FontAwesomeIcon
:icon="['fas', 'plus']"
<Plus
class="mr-1.5 size-3.5"
/>
{{ $t('bots.access.addRule') }}
@@ -113,8 +112,7 @@
:aria-label="$t('bots.access.dragToReorder')"
:disabled="isReordering"
>
<FontAwesomeIcon
:icon="['fas', 'grip-vertical']"
<GripVertical
class="size-3.5"
/>
</button>
@@ -167,8 +165,7 @@
size="icon-sm"
@click="openEditDialog(rule)"
>
<FontAwesomeIcon
:icon="['fas', 'pen']"
<SquarePen
class="size-3.5"
/>
</Button>
@@ -183,8 +180,7 @@
size="icon-sm"
class="text-destructive hover:text-destructive"
>
<FontAwesomeIcon
:icon="['fas', 'trash']"
<Trash2
class="size-3.5"
/>
</Button>
@@ -335,8 +331,7 @@
@toggle="scopeOpen = ($event.target as HTMLDetailsElement).open"
>
<summary class="flex cursor-pointer items-center gap-1 text-xs font-medium text-foreground select-none list-none">
<FontAwesomeIcon
:icon="['fas', 'chevron-right']"
<ChevronRight
class="size-3 transition-transform group-open:rotate-90"
/>
{{ $t('bots.access.sourceScopeTitle') }}
@@ -401,8 +396,7 @@
<summary
class="cursor-pointer list-none text-xs font-medium text-muted-foreground hover:text-foreground select-none"
>
<FontAwesomeIcon
:icon="['fas', 'chevron-right']"
<ChevronRight
class="mr-0.5 inline size-3 transition-transform group-open/conversation-manual:rotate-90"
/>
{{ $t('bots.access.manualConversationIds') }}
@@ -520,7 +514,7 @@ import { useSortable } from '@vueuse/integrations/useSortable'
import { useI18n } from 'vue-i18n'
import { toast } from 'vue-sonner'
import { useQuery, useQueryCache } from '@pinia/colada'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import { Plus, GripVertical, SquarePen, Trash2, ChevronRight } from 'lucide-vue-next'
import {
Button,
Input,
@@ -31,9 +31,8 @@
class="shrink-0 text-xs"
:title="hasIssue ? issueTitle : undefined"
>
<FontAwesomeIcon
<LoaderCircle
v-if="isPending"
:icon="['fas', 'spinner']"
class="mr-1 size-3 animate-spin"
/>
{{ statusLabel }}
@@ -59,6 +58,7 @@ import {
AvatarFallback,
Badge,
} from '@memohai/ui'
import { LoaderCircle } from 'lucide-vue-next'
import { computed } from 'vue'
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
@@ -8,8 +8,7 @@
v-if="isLoading && configuredChannels.length === 0"
class="flex items-center justify-center h-full p-4"
>
<FontAwesomeIcon
:icon="['fas', 'spinner']"
<LoaderCircle
class="size-4 animate-spin text-muted-foreground"
/>
</div>
@@ -79,8 +78,7 @@
size="sm"
:disabled="unconfiguredChannels.length === 0 && !isLoading"
>
<FontAwesomeIcon
:icon="['fas', 'plus']"
<Plus
class="mr-2 size-3"
/>
{{ $t('bots.channels.addChannel') }}
@@ -136,6 +134,7 @@
</template>
<script setup lang="ts">
import { LoaderCircle, Plus } from 'lucide-vue-next'
import { computed, ref, watch } from 'vue'
import {
Button,
@@ -129,8 +129,7 @@
class="flex flex-col items-center justify-center py-12 text-center"
>
<div class="rounded-full bg-muted p-3 mb-4">
<FontAwesomeIcon
:icon="['fas', 'compress']"
<Minimize2
class="size-6 text-muted-foreground"
/>
</div>
@@ -231,6 +230,7 @@
</template>
<script setup lang="ts">
import { Minimize2 } from 'lucide-vue-next'
import { ref, reactive, computed, watch, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { toast } from 'vue-sonner'
@@ -24,8 +24,7 @@
size="sm"
:disabled="!unboundProviders.length"
>
<FontAwesomeIcon
:icon="['fas', 'plus']"
<Plus
class="mr-1.5"
/>
{{ $t('bots.email.addBinding') }}
@@ -211,6 +210,7 @@ import {
Switch,
} from '@memohai/ui'
import ConfirmPopover from '@/components/confirm-popover/index.vue'
import { Plus } from 'lucide-vue-next'
import { computed, ref, watch } from 'vue'
import { toast } from 'vue-sonner'
import { useI18n } from 'vue-i18n'
@@ -129,8 +129,7 @@
class="flex flex-col items-center justify-center py-12 text-center"
>
<div class="rounded-full bg-muted p-3 mb-4">
<FontAwesomeIcon
:icon="['fas', 'heartbeat']"
<HeartPulse
class="size-6 text-muted-foreground"
/>
</div>
@@ -235,6 +234,7 @@
</template>
<script setup lang="ts">
import { HeartPulse } from 'lucide-vue-next'
import { ref, reactive, computed, watch, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { toast } from 'vue-sonner'
+9 -12
View File
@@ -13,7 +13,7 @@
size="icon-xs"
aria-label="Search"
>
<FontAwesomeIcon :icon="['fas', 'magnifying-glass']" />
<Search />
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
@@ -82,8 +82,7 @@
size="sm"
@click="openCreateDialog"
>
<FontAwesomeIcon
:icon="['fas', 'plus']"
<Plus
class="mr-1.5"
/>
{{ $t('common.add') }}
@@ -309,9 +308,8 @@
v-if="probing"
class="mr-1.5"
/>
<FontAwesomeIcon
<RefreshCw
v-else
:icon="['fas', 'rotate']"
class="mr-1.5"
/>
{{ probing ? $t('mcp.probing') : $t('mcp.probe') }}
@@ -329,7 +327,7 @@
v-if="probeAuthRequired"
class="text-xs text-amber-600 bg-amber-50 dark:bg-amber-900/20 rounded-md p-3 flex items-center gap-2"
>
<FontAwesomeIcon :icon="['fas', 'lock']" />
<Lock />
{{ $t('mcp.authRequired') }}
</div>
@@ -415,7 +413,7 @@
size="icon-xs"
@click="copyText(oauthCallbackUrl); toast.success($t('common.copied'))"
>
<FontAwesomeIcon :icon="['far', 'copy']" />
<Copy />
</Button>
</div>
<p class="text-xs text-muted-foreground">
@@ -436,9 +434,8 @@
v-if="oauthDiscovering || oauthAuthorizing"
class="mr-1.5"
/>
<FontAwesomeIcon
<KeyRound
v-else
:icon="['fas', 'key']"
class="mr-1.5"
/>
{{ oauthDiscovering ? $t('mcp.oauth.discovering') : oauthAuthorizing ? $t('mcp.oauth.authorizing') : $t('mcp.oauth.authorize') }}
@@ -466,8 +463,7 @@
:key="tool.name"
class="flex items-start gap-2 py-1.5 px-2 rounded text-xs hover:bg-accent/50"
>
<FontAwesomeIcon
:icon="['fas', 'wrench']"
<Wrench
class="mt-1 text-muted-foreground shrink-0 text-xs"
/>
<div class="min-w-0">
@@ -504,7 +500,7 @@
>
<EmptyHeader>
<EmptyMedia variant="icon">
<FontAwesomeIcon :icon="['fas', 'plug']" />
<Plug />
</EmptyMedia>
</EmptyHeader>
<EmptyTitle>{{ $t('mcp.emptyTitle') }}</EmptyTitle>
@@ -639,6 +635,7 @@
</template>
<script setup lang="ts">
import { Search, Plus, RefreshCw, Lock, Copy, KeyRound, Wrench, Plug } from 'lucide-vue-next'
import { computed, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { toast } from 'vue-sonner'
@@ -18,8 +18,7 @@
:aria-label="$t('bots.memory.compact')"
@click="openCompactDialog"
>
<FontAwesomeIcon
:icon="['fas', 'brain']"
<Brain
class="size-3.5 text-primary"
/>
</Button>
@@ -32,8 +31,7 @@
:aria-label="$t('common.refresh')"
@click="loadMemories"
>
<FontAwesomeIcon
:icon="['fas', 'rotate']"
<RefreshCw
:class="{ 'animate-spin': loading }"
class="size-3.5"
/>
@@ -41,8 +39,7 @@
</div>
</div>
<div class="relative">
<FontAwesomeIcon
:icon="['fas', 'magnifying-glass']"
<Search
class="absolute left-2.5 top-1/2 -translate-y-1/2 size-3 text-muted-foreground"
/>
<Input
@@ -77,8 +74,7 @@
@click="selectMemory(item)"
>
<div class="flex items-center gap-2">
<FontAwesomeIcon
:icon="['fas', 'file-lines']"
<FileText
class="size-3 shrink-0 opacity-70"
/>
<span class="truncate pr-4">{{ formatDate(item.created_at) }}</span>
@@ -97,8 +93,7 @@
class="w-full h-8 text-xs"
@click="openNewMemoryDialog"
>
<FontAwesomeIcon
:icon="['fas', 'plus']"
<Plus
class="mr-2 size-3"
/>
{{ $t('bots.memory.newMemory') }}
@@ -112,8 +107,7 @@
<div class="flex-1 flex flex-col min-h-0">
<div class="p-3 border-b flex items-center justify-between bg-muted/30 shrink-0">
<div class="flex items-center gap-3 min-w-0">
<FontAwesomeIcon
:icon="['fas', 'file-lines']"
<FileText
class="size-4 text-muted-foreground shrink-0"
/>
<div class="min-w-0">
@@ -129,8 +123,7 @@
:aria-label="$t('common.copy')"
@click="copyToClipboard(selectedMemory.id)"
>
<FontAwesomeIcon
:icon="['fas', 'copy']"
<Copy
class="size-2.5"
/>
</button>
@@ -151,8 +144,7 @@
:disabled="actionLoading"
:aria-label="$t('common.delete')"
>
<FontAwesomeIcon
:icon="['far', 'trash-can']"
<Trash2
class="size-3.5"
/>
</Button>
@@ -223,8 +215,7 @@
class="flex-1 flex flex-col items-center justify-center text-muted-foreground p-8 text-center"
>
<div class="size-12 rounded-full bg-muted flex items-center justify-center mb-4">
<FontAwesomeIcon
:icon="['fas', 'brain']"
<Brain
class="size-6 opacity-20"
/>
</div>
@@ -260,8 +251,7 @@
class="text-xs h-8"
@click="loadHistory"
>
<FontAwesomeIcon
:icon="['fas', 'rotate']"
<RefreshCw
:class="{ 'animate-spin': historyLoading }"
class="mr-1.5 size-3"
/>
@@ -292,9 +282,8 @@
class="mt-1 size-4 shrink-0 rounded border border-primary flex items-center justify-center transition-colors"
:class="selectedHistoryMessages.includes(msg) ? 'bg-primary text-primary-foreground' : 'bg-background'"
>
<FontAwesomeIcon
<Check
v-if="selectedHistoryMessages.includes(msg)"
:icon="['fas', 'check']"
class="size-2.5"
/>
</div>
@@ -444,6 +433,7 @@
</template>
<script setup lang="ts">
import { Brain, RefreshCw, Search, FileText, Plus, Copy, Trash2, Check } from 'lucide-vue-next'
import { computed, ref, onMounted, watch } from 'vue'
import { use } from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
@@ -45,8 +45,7 @@
class="flex flex-col items-center justify-center py-12 text-center"
>
<div class="rounded-full bg-muted p-3 mb-4">
<FontAwesomeIcon
:icon="['fas', 'calendar-alt']"
<Calendar
class="size-6 text-muted-foreground"
/>
</div>
@@ -159,6 +158,7 @@
</template>
<script setup lang="ts">
import { Calendar } from 'lucide-vue-next'
import { ref, computed, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { toast } from 'vue-sonner'
@@ -11,8 +11,7 @@
size="sm"
@click="handleCreate"
>
<FontAwesomeIcon
:icon="['fas', 'plus']"
<Plus
class="mr-2"
/>
{{ $t('bots.skills.addSkill') }}
@@ -34,8 +33,7 @@
class="flex flex-col items-center justify-center py-12 text-center"
>
<div class="rounded-full bg-muted p-3 mb-4">
<FontAwesomeIcon
:icon="['fas', 'bolt']"
<Zap
class="size-6 text-muted-foreground"
/>
</div>
@@ -73,8 +71,7 @@
:title="$t('common.edit')"
@click="handleEdit(skill)"
>
<FontAwesomeIcon
:icon="['fas', 'pen-to-square']"
<SquarePen
class="size-3.5"
/>
</Button>
@@ -91,8 +88,7 @@
:disabled="isDeleting"
:title="$t('common.delete')"
>
<FontAwesomeIcon
:icon="['far', 'trash-can']"
<Trash2
class="size-3.5"
/>
</Button>
@@ -149,6 +145,7 @@
</template>
<script setup lang="ts">
import { Plus, Zap, SquarePen, Trash2 } from 'lucide-vue-next'
import { ref, computed, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { toast } from 'vue-sonner'
@@ -18,24 +18,21 @@
class="w-full justify-between font-normal"
>
<span class="flex items-center gap-2 truncate">
<FontAwesomeIcon
<AppWindow
v-if="selected"
:icon="['fas', 'window-maximize']"
class="size-3.5 text-muted-foreground"
/>
<span class="truncate">{{ displayLabel || placeholder }}</span>
</span>
<FontAwesomeIcon
:icon="['fas', 'magnifying-glass']"
<Search
class="ml-2 size-3.5 shrink-0 text-muted-foreground"
/>
</Button>
</template>
<template #option-icon="{ option }">
<FontAwesomeIcon
<AppWindow
v-if="option.value"
:icon="['fas', 'window-maximize']"
class="size-3.5 text-muted-foreground"
/>
</template>
@@ -52,6 +49,7 @@
</template>
<script setup lang="ts">
import { AppWindow, Search } from 'lucide-vue-next'
import { Button } from '@memohai/ui'
import { computed } from 'vue'
import type { BrowsercontextsBrowserContext } from '@memohai/sdk'
@@ -150,8 +150,8 @@
:aria-pressed="!!visibleSecrets[key]"
@click="visibleSecrets[key] = !visibleSecrets[key]"
>
<FontAwesomeIcon
:icon="['fas', visibleSecrets[key] ? 'eye-slash' : 'eye']"
<component
:is="visibleSecrets[key] ? EyeOff : Eye"
class="size-3.5"
/>
</button>
@@ -262,6 +262,7 @@ import {
SelectContent,
SelectItem,
} from '@memohai/ui'
import { Eye, EyeOff } from 'lucide-vue-next'
import { reactive, watch, computed, ref } from 'vue'
import { toast } from 'vue-sonner'
import { useI18n } from 'vue-i18n'
@@ -3,8 +3,7 @@
<DialogTrigger as-child>
<slot name="trigger">
<Button variant="default">
<FontAwesomeIcon
:icon="['fas', 'plus']"
<Plus
class="mr-1.5"
/>
{{ $t('bots.createBot') }}
@@ -142,6 +141,7 @@ import {
SelectValue,
Spinner,
} from '@memohai/ui'
import { Plus } from 'lucide-vue-next'
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import z from 'zod'
@@ -18,24 +18,21 @@
class="w-full justify-between font-normal"
>
<span class="flex items-center gap-2 truncate">
<FontAwesomeIcon
<Brain
v-if="selected"
:icon="['fas', 'brain']"
class="size-3.5 text-primary"
/>
<span class="truncate">{{ displayLabel || placeholder }}</span>
</span>
<FontAwesomeIcon
:icon="['fas', 'magnifying-glass']"
<Search
class="ml-2 size-3.5 shrink-0 text-muted-foreground"
/>
</Button>
</template>
<template #option-icon="{ option }">
<FontAwesomeIcon
<Brain
v-if="option.value"
:icon="['fas', 'brain']"
class="size-3.5 text-primary"
/>
</template>
@@ -52,6 +49,7 @@
</template>
<script setup lang="ts">
import { Brain, Search } from 'lucide-vue-next'
import { Button } from '@memohai/ui'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
@@ -1,235 +0,0 @@
<template>
<div class="space-y-1">
<div
v-for="(msg, idx) in messages"
:key="msg.id || idx"
class="group relative rounded-lg border p-3 transition-colors hover:bg-muted/50"
>
<div class="flex items-start gap-3">
<!-- Role Icon -->
<div
class="flex size-8 shrink-0 items-center justify-center rounded-full"
:class="roleIconClass(msg.role)"
>
<FontAwesomeIcon
:icon="roleIcon(msg.role)"
class="size-3.5 text-white"
/>
</div>
<!-- Content -->
<div class="min-w-0 flex-1 space-y-1.5">
<!-- Top row -->
<div class="flex flex-wrap items-center gap-2 text-xs">
<Badge
:variant="roleBadgeVariant(msg.role)"
class="text-xs font-medium"
>
{{ roleLabel(msg.role) }}
</Badge>
<span
v-if="msg.sender_display_name"
class="font-medium truncate max-w-[200px]"
>
{{ msg.sender_display_name }}
</span>
<Badge
v-if="msg.platform"
variant="outline"
class="text-[10px] uppercase h-5"
>
{{ msg.platform }}
</Badge>
<span class="text-xs text-muted-foreground ml-auto shrink-0">
{{ formatTime(msg.created_at) }}
</span>
</div>
<!-- Message content -->
<div
class="text-xs leading-relaxed"
:class="{ 'font-mono text-xs': msg.role === 'tool' || msg.role === 'system' }"
>
<div
class="whitespace-pre-wrap break-words [overflow-wrap:anywhere]"
:class="{ 'line-clamp-4': !expandedIds.includes(msgKey(msg, idx)) }"
>
{{ formatContent(msg.content) }}
</div>
<button
v-if="isContentLong(msg.content)"
class="mt-1 text-xs text-primary hover:underline"
@click="toggleExpand(msgKey(msg, idx))"
>
{{ expandedIds.includes(msgKey(msg, idx)) ? collapseLabel : expandLabel }}
</button>
</div>
<!-- Usage -->
<div
v-if="hasUsage(msg.usage)"
class="flex items-center gap-3 text-xs text-muted-foreground pt-1"
>
<span class="inline-flex items-center gap-1">
<FontAwesomeIcon
:icon="['fas', 'chart-bar']"
class="size-3"
/>
{{ formatUsage(msg.usage) }}
</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { Badge } from '@memohai/ui'
import { formatDateTime } from '@/utils/date-time'
export interface MessageItem {
id?: string
role?: string
content?: unknown
sender_display_name?: string
sender_avatar_url?: string
platform?: string
created_at?: string
usage?: unknown
metadata?: Record<string, unknown>
[key: string]: unknown
}
defineProps<{
messages: MessageItem[]
}>()
const { t } = useI18n()
const expandedIds = ref<string[]>([])
const expandLabel = computed(() => t('bots.history.expandContent'))
const collapseLabel = computed(() => t('bots.history.collapseContent'))
function msgKey(msg: MessageItem, idx: number): string {
return msg.id || String(idx)
}
function toggleExpand(id: string) {
if (expandedIds.value.includes(id)) {
expandedIds.value = expandedIds.value.filter(v => v !== id)
} else {
expandedIds.value = [...expandedIds.value, id]
}
}
function roleIcon(role?: string): [string, string] {
switch (role) {
case 'user': return ['fas', 'user']
case 'assistant': return ['fas', 'robot']
case 'tool': return ['fas', 'wrench']
case 'system': return ['fas', 'laptop-code']
default: return ['fas', 'circle-question']
}
}
function roleIconClass(role?: string): string {
switch (role) {
case 'user': return 'bg-blue-500'
case 'assistant': return 'bg-emerald-500'
case 'tool': return 'bg-amber-500'
case 'system': return 'bg-slate-500'
default: return 'bg-muted-foreground'
}
}
function roleBadgeVariant(role?: string): 'default' | 'secondary' | 'destructive' | 'outline' {
switch (role) {
case 'user': return 'default'
case 'assistant': return 'secondary'
case 'tool': return 'outline'
case 'system': return 'outline'
default: return 'outline'
}
}
function roleLabel(role?: string): string {
const key = `bots.history.role.${role || 'system'}`
const val = t(key)
return val !== key ? val : (role || 'unknown')
}
function formatTime(val?: string): string {
return formatDateTime(val, { fallback: '-' })
}
function formatContent(content: unknown): string {
if (!content) return ''
try {
if (Array.isArray(content)) {
const decoder = new TextDecoder()
const str = decoder.decode(new Uint8Array(content as number[]))
try {
const parsed = JSON.parse(str)
if (typeof parsed === 'string') return parsed
return JSON.stringify(parsed, null, 2)
} catch {
return str
}
}
if (typeof content === 'string') return content
if (typeof content === 'object') return JSON.stringify(content, null, 2)
return String(content)
} catch {
return String(content)
}
}
function isContentLong(content: unknown): boolean {
const text = formatContent(content)
return text.length > 300 || text.split('\n').length > 4
}
function hasUsage(usage: unknown): boolean {
if (!usage) return false
if (Array.isArray(usage) && usage.length > 0) return true
if (typeof usage === 'object' && Object.keys(usage as object).length > 0) return true
return false
}
function formatUsage(usage: unknown): string {
if (!usage) return ''
try {
if (Array.isArray(usage)) {
const decoder = new TextDecoder()
const str = decoder.decode(new Uint8Array(usage as number[]))
try {
const parsed = JSON.parse(str)
if (typeof parsed === 'object' && parsed !== null) {
const parts: string[] = []
for (const [k, v] of Object.entries(parsed)) {
parts.push(`${k}: ${v}`)
}
return parts.join(' | ')
}
return str
} catch {
return str
}
}
if (typeof usage === 'object') {
const parts: string[] = []
for (const [k, v] of Object.entries(usage as Record<string, unknown>)) {
parts.push(`${k}: ${v}`)
}
return parts.join(' | ')
}
return String(usage)
} catch {
return ''
}
}
</script>
@@ -25,8 +25,7 @@
/>
<span class="truncate">{{ displayLabel || placeholder }}</span>
</span>
<FontAwesomeIcon
:icon="['fas', 'magnifying-glass']"
<Search
class="ml-2 size-3.5 shrink-0 text-muted-foreground"
/>
</Button>
@@ -52,6 +51,7 @@
</template>
<script setup lang="ts">
import { Search } from 'lucide-vue-next'
import { Button } from '@memohai/ui'
import { computed } from 'vue'
import type { SearchprovidersGetResponse } from '@memohai/sdk'
@@ -17,24 +17,21 @@
class="w-full justify-between font-normal"
>
<span class="flex items-center gap-2 truncate">
<FontAwesomeIcon
<Volume2
v-if="selected"
:icon="['fas', 'volume-high']"
class="size-3.5 text-muted-foreground"
/>
<span class="truncate">{{ displayLabel || placeholder }}</span>
</span>
<FontAwesomeIcon
:icon="['fas', 'magnifying-glass']"
<Search
class="ml-2 size-3.5 shrink-0 text-muted-foreground"
/>
</Button>
</template>
<template #option-icon="{ option }">
<FontAwesomeIcon
<Volume2
v-if="option.value"
:icon="['fas', 'volume-high']"
class="size-3.5 text-muted-foreground"
/>
</template>
@@ -51,6 +48,7 @@
</template>
<script setup lang="ts">
import { Volume2, Search } from 'lucide-vue-next'
import { Button } from '@memohai/ui'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
@@ -1,84 +0,0 @@
<template>
<SearchableSelectPopover
v-model="selected"
:options="options"
:placeholder="placeholder || ''"
:aria-label="placeholder || 'Select TTS provider'"
:search-placeholder="$t('speech.searchPlaceholder')"
search-aria-label="Search TTS providers"
:empty-text="$t('speech.emptyTitle')"
:show-group-headers="false"
>
<template #trigger="{ open, displayLabel }">
<Button
variant="outline"
role="combobox"
:aria-expanded="open"
:aria-label="placeholder || 'Select TTS provider'"
class="w-full justify-between font-normal"
>
<span class="flex items-center gap-2 truncate">
<FontAwesomeIcon
v-if="selected"
:icon="['fas', 'volume-high']"
class="size-3.5 text-muted-foreground"
/>
<span class="truncate">{{ displayLabel || placeholder }}</span>
</span>
<FontAwesomeIcon
:icon="['fas', 'magnifying-glass']"
class="ml-2 size-3.5 shrink-0 text-muted-foreground"
/>
</Button>
</template>
<template #option-icon="{ option }">
<FontAwesomeIcon
v-if="option.value"
:icon="['fas', 'volume-high']"
class="size-3.5 text-muted-foreground"
/>
</template>
<template #option-label="{ option }">
<span
class="truncate"
:class="{ 'text-muted-foreground': !option.value }"
>
{{ option.label }}
</span>
</template>
</SearchableSelectPopover>
</template>
<script setup lang="ts">
import { Button } from '@memohai/ui'
import { computed } from 'vue'
import type { TtsProviderResponse } from '@memohai/sdk'
import { useI18n } from 'vue-i18n'
import SearchableSelectPopover from '@/components/searchable-select-popover/index.vue'
import type { SearchableSelectOption } from '@/components/searchable-select-popover/index.vue'
const props = defineProps<{
providers: TtsProviderResponse[]
placeholder?: string
}>()
const { t } = useI18n()
const selected = defineModel<string>({ default: '' })
const options = computed<SearchableSelectOption[]>(() => {
const noneOption: SearchableSelectOption = {
value: '',
label: t('common.none'),
keywords: [t('common.none')],
}
const providerOptions = props.providers.map((provider) => ({
value: provider.id || '',
label: provider.name || provider.id || '',
description: provider.provider,
keywords: [provider.name ?? '', provider.provider ?? ''],
}))
return [noneOption, ...providerOptions]
})
</script>
@@ -24,9 +24,8 @@
v-if="isStarting"
class="mr-1.5"
/>
<FontAwesomeIcon
<QrCode
v-else
:icon="['fas', 'qrcode']"
class="mr-1.5 size-3.5"
/>
{{ $t('bots.channels.weixinQr.startScan') }}
@@ -57,8 +56,7 @@
class="absolute inset-0 flex items-center justify-center rounded-lg bg-background/80"
>
<div class="text-center">
<FontAwesomeIcon
:icon="['fas', 'mobile-screen']"
<Smartphone
class="size-8 text-primary mb-2"
/>
<p class="text-xs font-medium text-foreground">
@@ -103,8 +101,7 @@
class="flex flex-col items-center gap-3 py-4"
>
<div class="flex size-12 items-center justify-center rounded-full bg-green-100 dark:bg-green-900">
<FontAwesomeIcon
:icon="['fas', 'check']"
<Check
class="size-5 text-green-600 dark:text-green-400"
/>
</div>
@@ -132,6 +129,7 @@
</template>
<script setup lang="ts">
import { QrCode, Smartphone, Check } from 'lucide-vue-next'
import { ref, computed, onUnmounted } from 'vue'
import { Button, Spinner } from '@memohai/ui'
import { useI18n } from 'vue-i18n'
+4 -6
View File
@@ -21,8 +21,7 @@
:disabled="!bot || botLifecyclePending"
@click="handleEditAvatar"
>
<FontAwesomeIcon
:icon="['fas', 'pen-to-square']"
<SquarePen
class="size-6 text-white"
/>
</button>
@@ -70,8 +69,7 @@
:aria-label="$t('common.edit')"
@click="handleStartEditBotName"
>
<FontAwesomeIcon
:icon="['fas', 'pen-to-square']"
<SquarePen
class="size-3.5"
/>
</Button>
@@ -84,9 +82,8 @@
class="text-xs"
:title="hasIssue ? issueTitle : undefined"
>
<FontAwesomeIcon
<LoaderCircle
v-if="bot.status === 'creating' || bot.status === 'deleting'"
:icon="['fas', 'spinner']"
class="mr-1 size-3 animate-spin"
/>
{{ statusLabel }}
@@ -218,6 +215,7 @@ import {
SidebarMenuItem,
Toggle
} from '@memohai/ui'
import { SquarePen, LoaderCircle } from 'lucide-vue-next'
import { computed, ref, watch, onMounted, toValue } from 'vue'
import { useRoute } from 'vue-router'
import { toast } from 'vue-sonner'
+3 -3
View File
@@ -7,8 +7,7 @@
</h2>
<div class="flex items-center gap-3 ">
<div class="relative">
<FontAwesomeIcon
:icon="['fas', 'magnifying-glass']"
<Search
class="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground size-3.5"
/>
<Input
@@ -41,7 +40,7 @@
>
<EmptyHeader>
<EmptyMedia variant="icon">
<FontAwesomeIcon :icon="['fas', 'robot']" />
<Bot />
</EmptyMedia>
</EmptyHeader>
<EmptyTitle>{{ $t('bots.emptyTitle') }}</EmptyTitle>
@@ -61,6 +60,7 @@ import {
EmptyMedia,
EmptyTitle,
} from '@memohai/ui'
import { Search, Bot } from 'lucide-vue-next'
import { ref, computed, watch, onUnmounted } from 'vue'
import BotCard from './components/bot-card.vue'
import CreateBot from './components/create-bot.vue'
@@ -14,8 +14,7 @@
class="w-full shadow-none! text-muted-foreground mb-4"
variant="outline"
>
<FontAwesomeIcon
:icon="['fas', 'plus']"
<Plus
class="mr-1"
/> {{ $t('browser.add') }}
</Button>
@@ -62,6 +61,7 @@ import { useMutation, useQueryCache } from '@pinia/colada'
import { postBrowserContexts } from '@memohai/sdk'
import type { BrowsercontextsCreateRequest } from '@memohai/sdk'
import { useI18n } from 'vue-i18n'
import { Plus } from 'lucide-vue-next'
import FormDialogShell from '@/components/form-dialog-shell/index.vue'
import { useDialogMutation } from '@/composables/useDialogMutation'
@@ -2,8 +2,7 @@
<div class="p-4">
<section class="flex justify-between items-center">
<div class="flex items-center gap-2">
<FontAwesomeIcon
:icon="['fas', 'window-maximize']"
<AppWindow
class="size-5"
/>
<div>
@@ -212,7 +211,7 @@
variant="outline"
:aria-label="$t('common.delete')"
>
<FontAwesomeIcon :icon="['far', 'trash-can']" />
<Trash2 />
</Button>
</template>
</ConfirmPopover>
@@ -252,6 +251,7 @@ import { useDialogMutation } from '@/composables/useDialogMutation'
import ConfirmPopover from '@/components/confirm-popover/index.vue'
import LoadingButton from '@/components/loading-button/index.vue'
import { resolveApiErrorMessage } from '@/utils/api-error'
import { AppWindow, Trash2 } from 'lucide-vue-next'
import { emptyTimezoneValue } from '@/utils/timezones'
import TimezoneSelect from '@/components/timezone-select/index.vue'
+4 -5
View File
@@ -19,6 +19,7 @@ import { getBrowserContexts } from '@memohai/sdk'
import type { BrowsercontextsBrowserContext } from '@memohai/sdk'
import AddBrowserContext from './components/add-browser-context.vue'
import ContextSetting from './components/context-setting.vue'
import { AppWindow, Plus } from 'lucide-vue-next'
import MasterDetailSidebarLayout from '@/components/master-detail-sidebar-layout/index.vue'
const { data: contextData } = useQuery({
@@ -77,8 +78,7 @@ const openStatus = reactive({
}
}"
>
<FontAwesomeIcon
:icon="['fas', 'window-maximize']"
<AppWindow
class="mr-2"
/>
{{ item.name }}
@@ -105,7 +105,7 @@ const openStatus = reactive({
>
<EmptyHeader>
<EmptyMedia variant="icon">
<FontAwesomeIcon :icon="['fas', 'window-maximize']" />
<AppWindow />
</EmptyMedia>
</EmptyHeader>
<EmptyTitle>{{ $t('browser.emptyTitle') }}</EmptyTitle>
@@ -115,8 +115,7 @@ const openStatus = reactive({
variant="outline"
@click="openStatus.addOpen = true"
>
<FontAwesomeIcon
:icon="['fas', 'plus']"
<Plus
class="mr-1"
/> {{ $t('browser.add') }}
</Button>
@@ -14,8 +14,7 @@
class="w-full shadow-none! text-muted-foreground mb-4"
variant="outline"
>
<FontAwesomeIcon
:icon="['fas', 'plus']"
<Plus
class="mr-1"
/> {{ $t('email.add') }}
</Button>
@@ -99,6 +98,7 @@ import { useMutation, useQuery, useQueryCache } from '@pinia/colada'
import { postEmailProviders, getEmailProvidersMeta } from '@memohai/sdk'
import type { EmailCreateProviderRequest } from '@memohai/sdk'
import { useI18n } from 'vue-i18n'
import { Plus } from 'lucide-vue-next'
import FormDialogShell from '@/components/form-dialog-shell/index.vue'
import { useDialogMutation } from '@/composables/useDialogMutation'
@@ -2,8 +2,7 @@
<div class="p-4">
<section class="flex justify-between items-center">
<div class="flex items-center gap-2">
<FontAwesomeIcon
:icon="['fas', 'envelope']"
<Mail
class="size-5"
/>
<div>
@@ -78,8 +77,8 @@
class="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
@click="visibleSecrets[field.key] = !visibleSecrets[field.key]"
>
<FontAwesomeIcon
:icon="['fas', visibleSecrets[field.key] ? 'eye-slash' : 'eye']"
<component
:is="visibleSecrets[field.key] ? EyeOff : Eye"
class="size-3.5"
/>
</button>
@@ -170,8 +169,7 @@
:loading="authorizeLoading"
@click="handleAuthorize"
>
<FontAwesomeIcon
:icon="['fas', 'key']"
<KeyRound
class="mr-1.5"
/>
{{ $t('email.oauth.authorize') }}
@@ -200,7 +198,7 @@
type="button"
variant="outline"
>
<FontAwesomeIcon :icon="['far', 'trash-can']" />
<Trash2 />
</Button>
</template>
</ConfirmPopover>
@@ -231,6 +229,7 @@ import {
Switch,
Label,
} from '@memohai/ui'
import { Mail, Eye, EyeOff, KeyRound, Trash2 } from 'lucide-vue-next'
import ConfirmPopover from '@/components/confirm-popover/index.vue'
import LoadingButton from '@/components/loading-button/index.vue'
import { computed, inject, reactive, ref, watch } from 'vue'
+3 -3
View File
@@ -19,6 +19,7 @@ import { getEmailProviders } from '@memohai/sdk'
import type { EmailProviderResponse } from '@memohai/sdk'
import AddEmailProvider from './components/add-email-provider.vue'
import ProviderSetting from './components/provider-setting.vue'
import { Mail, Plus } from 'lucide-vue-next'
import MasterDetailSidebarLayout from '@/components/master-detail-sidebar-layout/index.vue'
const { data: providerData } = useQuery({
@@ -100,7 +101,7 @@ const openStatus = reactive({ addOpen: false })
>
<EmptyHeader>
<EmptyMedia variant="icon">
<FontAwesomeIcon :icon="['fas', 'envelope']" />
<Mail />
</EmptyMedia>
</EmptyHeader>
<EmptyTitle>{{ $t('email.emptyTitle') }}</EmptyTitle>
@@ -110,8 +111,7 @@ const openStatus = reactive({ addOpen: false })
variant="outline"
@click="openStatus.addOpen = true"
>
<FontAwesomeIcon
:icon="['fas', 'plus']"
<Plus
class="mr-1"
/> {{ $t('email.add') }}
</Button>
@@ -51,17 +51,14 @@
:title="getContainerPath(att)"
@click="handleOpenContainerFile(att)"
>
<FontAwesomeIcon
:icon="['fas', fileIcon(att)]"
<component
:is="fileIconComponent(att)"
class="size-4 text-muted-foreground"
/>
<span class="truncate max-w-[200px] font-mono text-xs">
{{ getDisplayName(att) }}
</span>
<FontAwesomeIcon
:icon="['fas', 'arrow-up-right-from-square']"
class="size-3 text-muted-foreground/60 shrink-0"
/>
<ExternalLink class="size-3 text-muted-foreground/60 shrink-0" />
</button>
<!-- Downloadable file -->
@@ -72,8 +69,8 @@
rel="noopener noreferrer"
class="flex items-center gap-2 px-3 py-2 rounded-lg border bg-muted/30 hover:bg-muted/60 transition-colors text-xs"
>
<FontAwesomeIcon
:icon="['fas', fileIcon(att)]"
<component
:is="fileIconComponent(att)"
class="size-4 text-muted-foreground"
/>
<span class="truncate max-w-[200px]">
@@ -86,8 +83,8 @@
v-else
class="flex items-center gap-2 px-3 py-2 rounded-lg border bg-muted/30 text-xs text-muted-foreground"
>
<FontAwesomeIcon
:icon="['fas', fileIcon(att)]"
<component
:is="fileIconComponent(att)"
class="size-4"
/>
<span class="truncate max-w-[200px]">
@@ -100,6 +97,8 @@
<script setup lang="ts">
import { inject } from 'vue'
import { Music, Video, File as FileIcon, ExternalLink } from 'lucide-vue-next'
import type { Component } from 'vue'
import type { AttachmentBlock, AttachmentItem } from '@/store/chat-list'
import { resolveUrl } from '../composables/useMediaGallery'
import { openInFileManagerKey } from '../composables/useFileManagerProvider'
@@ -164,10 +163,10 @@ function handleOpenContainerFile(att: AttachmentItem) {
}
}
function fileIcon(att: AttachmentItem): string {
function fileIconComponent(att: AttachmentItem): Component {
const type = String(att.type ?? '').toLowerCase()
if (type === 'audio' || type === 'voice') return 'music'
if (type === 'video') return 'video'
return 'file'
if (type === 'audio' || type === 'voice') return Music
if (type === 'video') return Video
return FileIcon
}
</script>
@@ -1,153 +0,0 @@
<template>
<section>
<SidebarMenuButton
as-child
class="justify-start py-5! px-4"
:disabled="bot.status === 'error'"
>
<Toggle
:class="`p-2.75! border hover:border-border hover:bg-(--bot-item)! ${isActive ? 'border-border bg-(--bot-item)!' : 'border-transparent bg-transparent!'} h-[initial]! ${currentBotId === bot.id ? 'border-inherit' : ''}`"
:model-value="isActive"
@click="handleSelect(bot)"
>
<section
v-if="bot.status === 'loading'"
class="flex gap-2 overflow-hidden w-full items-center "
>
<ChatStatus />
<section class="flex flex-col gap-0.5 flex-1">
<h5 class="text-xs flex ">
<span class="truncate flex-1 max-w-30">
{{ bot.display_name || bot.id }}
</span>
<time
class="ml-auto flex-none"
:datetime="create_date"
>{{ create_date }}</time>
</h5>
<p class="text-xs text-muted-foreground min-w-0 ">
<i class="w-1.25 aspect-square bg-blue-500 rounded-full inline-block" />
推理中
</p>
</section>
</section>
<section
v-if="bot.status === 'ready'"
class="flex gap-2 overflow-hidden w-full"
>
<ChatStatus :is-loading="false" />
<section class="flex flex-col gap-0.5 flex-1">
<section>
<h5 class="text-xs flex flex-1">
<span class="truncate flex-1 max-w-30">
{{ bot.display_name || bot.id }}
</span>
<time
class="ml-auto "
:datetime="create_date"
>{{ create_date }}</time>
</h5>
</section>
<p class="text-xs text-muted-foreground min-w-0 ">
fewfwefewfewfewfwf
</p>
</section>
</section>
<section
v-if="bot.status === 'error'"
class="flex gap-2 overflow-hidden w-full"
>
<ChatStatus
:is-loading="false"
:error="bot.status === 'error'"
/>
<section class="flex flex-col gap-0.5 flex-1">
<h5 class="text-xs flex">
<span class="truncate flex-1 max-w-30">
{{ bot.display_name || bot.id }}
</span>
<time
class="ml-auto"
:datetime="create_date"
>{{ create_date }}</time>
</h5>
<p class="text-xs text-muted-foreground min-w-0 ">
fewfwefewfewfewfwf
</p>
</section>
</section>
<section
v-if="bot.status === 'no-setting'"
class="flex gap-2 overflow-hidden w-full "
>
<ChatStatus :is-loading="false" />
<section class="self-center flex-1">
<h5 class="text-xs flex">
<span class="truncate flex-1 max-w-30">
{{ bot.display_name || bot.id }}
</span>
<time
class="ml-auto"
:datetime="create_date"
>{{ create_date }}</time>
</h5>
</section>
</section>
</Toggle>
<!-- <Toggle
v-if="bot.status === 'ready'"
:class="`p-2.75! border border-border h-[initial]! bg-(--bot-item)! ${currentBotId === bot.id ? 'border-inherit' : ''}`"
:model-value="isActive(bot.id as string).value"
@click="handleSelect(bot)"
>
<section class="flex gap-2 overflow-hidden w-full">
<ChatStatus />
<section class="flex flex-col gap-0.5">
<h5 class="text-xs">
{{ bot.display_name || bot.id }}
</h5>
<p class="text-xs text-muted-foreground min-w-0 ">
<i class="w-1.25 aspect-square bg-blue-500 rounded-full inline-block" />
推理中
</p>
</section>
</section>
</Toggle> -->
</SidebarMenuButton>
</section>
</template>
<script setup lang="ts">
import type { BotsBot } from '@memohai/sdk'
import { computed } from 'vue'
import { storeToRefs } from 'pinia'
import { useChatStore } from '@/store/chat-list'
import ChatStatus from '@/components/chat/chat-status/index.vue'
import {
Toggle,
SidebarMenuButton,
} from '@memohai/ui'
import moment from 'moment'
const { bot } = defineProps<{ bot: BotsBot }>()
const chatStore = useChatStore()
const create_date = computed(() => {
return moment(bot?.updated_at ?? Date.now()).format('hh:ss')
})
const { currentBotId } = storeToRefs(chatStore)
const isActive = computed(() => {
return currentBotId.value === bot.id
})
function handleSelect(bot: BotsBot) {
chatStore.selectBot(bot.id)
}
</script>
@@ -1,192 +0,0 @@
<template>
<section :class="mountNode.id">
<Teleport :to="mountNode.leftDefault">
<SidebarProvider
:open="sidebarOpen"
>
<SidebarInset>
<Sidebar
class="absolute! "
collapsible="icon"
>
<SidebarHeader
class="h-12 flex flex-rows justify-center"
>
<section class="flex items-center gap-2">
<img
src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTgiIGhlaWdodD0iMTgiIHZpZXdCb3g9IjAgMCAxOCAxOCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEuNSA5QzEuNSA2LjE4NzcgMS41IDQuNzgxNTUgMi4yMTYxOSAzLjc5NTgxQzIuNDQ3NDggMy40Nzc0NSAyLjcyNzQ1IDMuMTk3NDggMy4wNDU4MSAyLjk2NjE5QzQuMDMxNTUgMi4yNSA1LjQzNzcgMi4yNSA4LjI1IDIuMjVIOS43NUMxMi41NjIzIDIuMjUgMTMuOTY4NCAyLjI1IDE0Ljk1NDIgMi45NjYxOUMxNS4yNzI2IDMuMTk3NDggMTUuNTUyNSAzLjQ3NzQ1IDE1Ljc4MzggMy43OTU4MUMxNi41IDQuNzgxNTUgMTYuNSA2LjE4NzcgMTYuNSA5QzE2LjUgMTEuODEyMyAxNi41IDEzLjIxODQgMTUuNzgzOCAxNC4yMDQyQzE1LjU1MjUgMTQuNTIyNSAxNS4yNzI2IDE0LjgwMjUgMTQuOTU0MiAxNS4wMzM4QzEzLjk2ODQgMTUuNzUgMTIuNTYyMyAxNS43NSA5Ljc1IDE1Ljc1SDguMjVDNS40Mzc3IDE1Ljc1IDQuMDMxNTUgMTUuNzUgMy4wNDU4MSAxNS4wMzM4QzIuNzI3NDUgMTQuODAyNSAyLjQ0NzQ4IDE0LjUyMjUgMi4yMTYxOSAxNC4yMDQyQzEuNSAxMy4yMTg0IDEuNSAxMS44MTIzIDEuNSA5WiIgc3Ryb2tlPSIjMEEwQTBBIiBzdHJva2Utd2lkdGg9IjEuNSIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPgo8cGF0aCBkPSJNNy4xMjUgMi42MjVMNy4xMjUgMTUuMzc1IiBzdHJva2U9IiMwQTBBMEEiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+CjxwYXRoIGQ9Ik0zLjc1IDUuMjVDMy43NSA1LjI1IDQuNDM1NjYgNS4yNSA0Ljg3NSA1LjI1IiBzdHJva2U9IiMwQTBBMEEiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz4KPHBhdGggZD0iTTMuNzUgOC4yNUg0Ljg3NSIgc3Ryb2tlPSIjMEEwQTBBIiBzdHJva2Utd2lkdGg9IjEuNSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+CjxwYXRoIGQ9Ik0xMi43NSA3LjVMMTEuODMwMSA4LjI5Mjg5QzExLjQ0MzQgOC42MjYyMyAxMS4yNSA4Ljc5Mjg5IDExLjI1IDlDMTEuMjUgOS4yMDcxMSAxMS40NDM0IDkuMzczNzcgMTEuODMwMSA5LjcwNzExTDEyLjc1IDEwLjUiIHN0cm9rZT0iIzBBMEEwQSIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPgo8L3N2Zz4K"
alt="折叠"
class="group-data-[collapsible=icon]:m-auto"
@click="sidebarOpen = !sidebarOpen"
>
<h3 class="font-medium text-xs group-data-[collapsible=icon]:hidden">
{{ $t('sidebar.session') }}
</h3>
</section>
</SidebarHeader>
<Separator />
<section
class="not-group-data-[collapsible=icon]:hidden flex justify-center"
style="writing-mode:sideways-rl"
>
<span class="flex-none w-[1em] mr-2.5 mt-4 text-muted-foreground text-xs">
{{ $t('sidebar.session') }}
</span>
</section>
<SidebarContent>
<SidebarGroup class="group-data-[collapsible=icon]:invisible">
<SidebarGroupContent>
<SidebarMenu class="my-4">
<InputGroup>
<InputGroupInput placeholder="Search..." />
<InputGroupAddon>
<FontAwesomeIcon :icon="['fas', 'magnifying-glass']" />
</InputGroupAddon>
</InputGroup>
</SidebarMenu>
<h4 class="text-xs uppercase text-muted-foreground tracking-wide mb-2">
我的SESSION
</h4>
<SidebarMenu ref="session-item">
<SidebarMenuItem
v-for="bot in bots"
:key="bot.id"
class="relative hover:[&_svg]:visible"
>
<!-- <SidebarMenuButton
tooltip="afwef"
class="py-5 text-muted-foreground relative before:absolute before:w-0.5! before:top-2 before:bottom-2 data-[active=true]:before:bg-[#8B56E3] hover:before:bg-[#8B56E3] before:left-0.5!"
>
<FontAwesomeIcon :icon="['fas', 'grip-vertical']" />
<span>fwefwef</span>
</SidebarMenuButton> -->
<FontAwesomeIcon
:icon="['fas', 'grip-vertical']"
class="can-dragable cursor-pointer absolute top-0 bottom-0 m-auto w-1.5! left-1! invisible z-100 text-[#C7C7C7]!"
/>
<BotItem :bot="bot" />
</SidebarMenuItem>
</SidebarMenu>
<SidebarMenu>
<div
v-if="isLoading"
class="flex justify-center py-4"
>
<FontAwesomeIcon
:icon="['fas', 'spinner']"
class="size-4 animate-spin text-muted-foreground"
/>
</div>
<div
v-if="!isLoading && bots.length === 0"
class="px-3 py-6 text-center text-xs text-muted-foreground"
>
{{ $t('bots.emptyTitle') }}
</div>
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
<SidebarFooter class="group-data-[collapsible=icon]:invisible">
<Button class="mb-4 justify-start gap-4">
<FontAwesomeIcon :icon="['fas', 'plus']" />
Session
</Button>
</SidebarFooter>
</Sidebar>
</SidebarInset>
</SidebarProvider>
</Teleport>
<section class="hidden-clip-section" />
</section>
</template>
<script setup lang="ts">
import { computed, useTemplateRef, watch,ref } from 'vue'
import { useQuery } from '@pinia/colada'
import { getBotsQuery } from '@memohai/sdk/colada'
import type { BotsBot } from '@memohai/sdk'
import Sortable from 'sortablejs'
import {
SidebarMenu,
SidebarMenuItem,
SidebarHeader,
SidebarProvider,
SidebarContent,
Sidebar,
SidebarInset,
Separator,
SidebarFooter,
Button,
InputGroup,
InputGroupInput,
InputGroupAddon,
SidebarGroup,
SidebarGroupContent
} from '@memohai/ui'
import BotItem from './bot-item.vue'
import useControlVisibleStatus from '@/utils/useControlVisibleStatus'
const sidebarOpen = ref(true)
const sessionItem = useTemplateRef('session-item')
watch(sessionItem, () => {
const el = sessionItem.value?.$el
if (sessionItem.value?.$el) {
new Sortable(el, {
animation: 150,
handle: '.can-dragable'
})
}
}, {
immediate: true
})
console.log(Sortable)
const { data: botData, isLoading } = useQuery(getBotsQuery())
const bots = computed<BotsBot[]>(() => botData.value?.items?.concat({
'id': '991cd528-0c10-41a0-93e6-a6a7006a433cd',
'owner_user_id': '9b7390f6-a336-4c76-bf58-df616942c9a6',
'type': 'personal',
'display_name': 'feafew',
'is_active': true,
'allow_guest': false,
'status': 'loading',
'check_state': 'ok',
'check_issue_count': 0,
'created_at': '2026-03-23T10:14:13.269928+08:00',
'updated_at': '2026-03-23T10:14:13.435601+08:00'
} as BotsBot, {
'id': '991cd528-0c10-41a0-93e6-a6a754006ac3cd',
'owner_user_id': '9b7390f6-a336-4c76-bf58-df616942c9a6',
'type': 'personal',
'display_name': 'feafew',
'is_active': true,
'allow_guest': false,
'status': 'error',
'check_state': 'ok',
'check_issue_count': 0,
'created_at': '2026-03-23T10:14:13.269928+08:00',
'updated_at': '2026-03-23T10:14:13.435601+08:00'
} as BotsBot, {
'id': '991cd528-0c10-41a0fewf-93e6-a6a754006ac3cd',
'owner_user_id': '9b7390f6-a336-4c76-bf58-df616942c9a6',
'type': 'personal',
'display_name': 'feafew',
'is_active': true,
'allow_guest': false,
'status': 'no-setting',
'check_state': 'ok',
'check_issue_count': 0,
'created_at': '2026-03-23T10:14:13.269928+08:00',
'updated_at': '2026-03-23T10:14:13.435601+08:00'
} as BotsBot) ?? [])
const mountNode = useControlVisibleStatus()
</script>
@@ -52,8 +52,7 @@
v-if="loadingOlder"
class="flex justify-center py-2"
>
<FontAwesomeIcon
:icon="['fas', 'spinner']"
<LoaderCircle
class="size-3.5 animate-spin text-muted-foreground"
/>
</div>
@@ -101,8 +100,8 @@
:key="i"
class="relative group flex items-center gap-1.5 px-2 py-1 rounded-md border bg-muted/40 text-xs"
>
<FontAwesomeIcon
:icon="['fas', file.type.startsWith('image/') ? 'image' : 'file']"
<component
:is="file.type.startsWith('image/') ? ImageIcon : FileIcon"
class="size-3 text-muted-foreground"
/>
<span class="truncate max-w-30">{{ file.name }}</span>
@@ -112,8 +111,7 @@
:aria-label="`${$t('common.delete')}: ${file.name}`"
@click="pendingFiles.splice(i, 1)"
>
<FontAwesomeIcon
:icon="['fas', 'xmark']"
<X
class="size-3"
/>
</button>
@@ -144,8 +142,7 @@
aria-label="Attach files"
@click="fileInput?.click()"
>
<FontAwesomeIcon
:icon="['fas', 'paperclip']"
<Paperclip
class="size-3.5"
/>
</Button>
@@ -157,8 +154,7 @@
:aria-label="$t('chat.files')"
@click="fileManagerOpen = true"
>
<FontAwesomeIcon
:icon="['fas', 'folder-open']"
<FolderOpen
class="size-3.5"
/>
</Button>
@@ -171,8 +167,7 @@
class="ml-auto bg-[#8B56E3]"
@click="handleSend"
>
<FontAwesomeIcon
:icon="['fas', 'paper-plane']"
<Send
class="size-2"
/>
{{ $t('chat.send') }}
@@ -186,8 +181,7 @@
aria-label="Stop generating response"
@click="chatStore.abort()"
>
<FontAwesomeIcon
:icon="['fas', 'spinner']"
<LoaderCircle
class="size-3.5 animate-spin"
/>
</Button>
@@ -225,6 +219,7 @@
<script setup lang="ts">
import { ref, computed, nextTick, onMounted, provide, useTemplateRef, watchEffect} from 'vue'
import { LoaderCircle, Image as ImageIcon, File as FileIcon, X, Paperclip, FolderOpen, Send } from 'lucide-vue-next'
import { ScrollArea, Button, InputGroup, InputGroupAddon, InputGroupTextarea, Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription,Separator } from '@memohai/ui'
import { useChatStore } from '@/store/chat-list'
import { storeToRefs } from 'pinia'
@@ -15,7 +15,7 @@
variant="ghost"
size="icon"
>
<FontAwesomeIcon :icon="['fas', 'arrow-rotate-right']" />
<RefreshCw />
</Button>
</div>
</div>
@@ -25,4 +25,5 @@
import ChatStatus from '@/components/chat/chat-status/index.vue'
import ChatStep from '@/components/chat/chat-step/index.vue'
import { Button } from '@memohai/ui'
import { RefreshCw } from 'lucide-vue-next'
</script>
@@ -19,8 +19,7 @@
aria-label="Close"
@click="close"
>
<FontAwesomeIcon
:icon="['fas', 'xmark']"
<X
class="size-6"
/>
</button>
@@ -33,8 +32,7 @@
aria-label="Previous"
@click.stop="prev"
>
<FontAwesomeIcon
:icon="['fas', 'chevron-left']"
<ChevronLeft
class="size-6"
/>
</button>
@@ -47,8 +45,7 @@
aria-label="Next"
@click.stop="next"
>
<FontAwesomeIcon
:icon="['fas', 'chevron-right']"
<ChevronRight
class="size-6"
/>
</button>
@@ -86,6 +83,7 @@
<script setup lang="ts">
import { computed, watchEffect, onUnmounted, nextTick, ref } from 'vue'
import { X, ChevronLeft, ChevronRight } from 'lucide-vue-next'
export interface MediaGalleryItem {
src: string
@@ -148,8 +148,7 @@
v-if="message.streaming && message.blocks.length === 0"
class="flex items-center gap-2 text-xs text-muted-foreground h-6"
>
<FontAwesomeIcon
:icon="['fas', 'spinner']"
<LoaderCircle
class="size-3.5 animate-spin"
/>
{{ $t('chat.thinking') }}
@@ -167,6 +166,7 @@
<script setup lang="ts">
import { computed } from 'vue'
import { LoaderCircle } from 'lucide-vue-next'
import { formatRelativeTime, formatDateTime } from '@/utils/date-time'
import { Avatar, AvatarImage, AvatarFallback } from '@memohai/ui'
import MarkdownRender, { enableKatex, enableMermaid } from 'markstream-vue'
@@ -22,8 +22,8 @@
v-else
class="flex items-center justify-center size-[26px] rounded-full bg-accent border border-border"
>
<FontAwesomeIcon
:icon="iconDef"
<component
:is="iconComponent"
class="size-2.5"
:class="iconClass"
/>
@@ -62,7 +62,8 @@
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { computed, type Component } from 'vue'
import { HeartPulse, Clock, GitBranch, MessageSquare } from 'lucide-vue-next'
import { useI18n } from 'vue-i18n'
import type { SessionSummary } from '@/composables/api/useChat'
import { Avatar, AvatarImage, AvatarFallback } from '@memohai/ui'
@@ -86,12 +87,12 @@ const isIMSession = computed(() => {
return ct !== '' && !WEB_CHANNELS.has(ct)
})
const iconDef = computed<string[]>(() => {
const iconComponent = computed<Component>(() => {
switch (props.session.type) {
case 'heartbeat': return ['fas', 'heart-pulse']
case 'schedule': return ['fas', 'clock']
case 'subagent': return ['fas', 'code-branch']
default: return ['fas', 'message']
case 'heartbeat': return HeartPulse
case 'schedule': return Clock
case 'subagent': return GitBranch
default: return MessageSquare
}
})
@@ -1,194 +0,0 @@
<template>
<div
class="flex flex-col h-full shrink-0 border-l border-border bg-sidebar transition-[width] duration-200"
:class="sidebarOpen ? 'w-[240px]' : 'w-10'"
>
<div class="h-12 flex items-center shrink-0 px-3">
<button
class="shrink-0"
@click="sidebarOpen = !sidebarOpen"
>
<img
src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTgiIGhlaWdodD0iMTgiIHZpZXdCb3g9IjAgMCAxOCAxOCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE2LjUgOUMxNi41IDYuMTg3NyAxNi41IDQuNzgxNTUgMTUuNzgzOCAzLjc5NTgxQzE1LjU1MjUgMy40Nzc0NSAxNS4yNzI2IDMuMTk3NDggMTQuOTU0MiAyLjk2NjE5QzEzLjk2ODQgMi4yNSAxMi41NjIzIDIuMjUgOS43NSAyLjI1SDguMjVDNS40Mzc3IDIuMjUgNC4wMzE1NSAyLjI1IDMuMDQ1ODEgMi45NjYxOUMyLjcyNzQ1IDMuMTk3NDggMi40NDc0OCAzLjQ3NzQ1IDIuMjE2MTkgMy43OTU4MUMxLjUgNC43ODE1NSAxLjUgNi4xODc3IDEuNSA5QzEuNSAxMS44MTIzIDEuNSAxMy4yMTg0IDIuMjE2MTkgMTQuMjA0MkMyLjQ0NzQ4IDE0LjUyMjUgMi43Mjc0NSAxNC44MDI1IDMuMDQ1ODEgMTUuMDMzOEM0LjAzMTU1IDE1Ljc1IDUuNDM3NyAxNS43NSA4LjI1IDE1Ljc1SDkuNzVDMTIuNTYyMyAxNS43NSAxMy45Njg0IDE1Ljc1IDE0Ljk1NDIgMTUuMDMzOEMxNS4yNzI2IDE0LjgwMjUgMTUuNTUyNSAxNC41MjI1IDE1Ljc4MzggMTQuMjA0MkMxNi41IDEzLjIxODQgMTYuNSAxMS44MTIzIDE2LjUgOVoiIHN0cm9rZT0iIzBBMEEwQSIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz4KPHBhdGggZD0iTTEwLjg3NSAyLjYyNUwxMC44NzUgMTUuMzc1IiBzdHJva2U9IiMwQTBBMEEiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+CjxwYXRoIGQ9Ik0xNC4yNSA1LjI1SDEzLjEyNSIgc3Ryb2tlPSIjMEEwQTBBIiBzdHJva2Utd2lkdGg9IjEuNSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+CjxwYXRoIGQ9Ik0xNC4yNSA4LjI1SDEzLjEyNSIgc3Ryb2tlPSIjMEEwQTBBIiBzdHJva2Utd2lkdGg9IjEuNSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+CjxwYXRoIGQ9Ik02IDcuNUw2LjkxOTkxIDguMjkyODlDNy4zMDY2MyA4LjYyNjIzIDcuNSA4Ljc5Mjg5IDcuNSA5QzcuNSA5LjIwNzExIDcuMzA2NjQgOS4zNzM3NyA2LjkxOTkxIDkuNzA3MTFMNiAxMC41IiBzdHJva2U9IiMwQTBBMEEiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz4KPC9zdmc+Cg=="
alt="toggle"
class="size-[18px] dark:invert"
>
</button>
<h3
v-if="sidebarOpen"
class="font-medium text-xs ml-2"
>
Session 源数据
</h3>
</div>
<Separator />
<template v-if="sidebarOpen">
<ScrollArea class="flex-1">
<form
class="p-3 **:[label]:text-xs **:[label]:uppercase **:[label]:text-muted-foreground **:[label]:tracking-wide"
>
<section class="flex flex-col gap-6">
<FormField
v-slot="{ componentField }"
name="bot"
>
<FormItem>
<FormLabel>Bot</FormLabel>
<FormControl>
<InputGroup>
<InputGroupInput v-bind="componentField" />
<InputGroupAddon>
<FontAwesomeIcon
:icon="['fas', 'wand-magic-sparkles']"
class="size-3.5 text-muted-foreground"
/>
</InputGroupAddon>
</InputGroup>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<FormField
v-slot="{ componentField }"
name="skills"
>
<FormItem>
<FormLabel>Top Skills</FormLabel>
<FormControl>
<InputGroup>
<InputGroupInput v-bind="componentField" />
<InputGroupAddon>
<FontAwesomeIcon
:icon="['fas', 'circle']"
class="size-1.5 text-muted-foreground"
/>
</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupInput v-bind="componentField" />
<InputGroupAddon>
<FontAwesomeIcon
:icon="['fas', 'circle']"
class="size-1.5 text-muted-foreground"
/>
</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupInput v-bind="componentField" />
<InputGroupAddon>
<FontAwesomeIcon
:icon="['fas', 'circle']"
class="size-1.5 text-muted-foreground"
/>
</InputGroupAddon>
</InputGroup>
</FormControl>
<p class="text-xs text-muted-foreground mt-4">
<FontAwesomeIcon
:icon="['fas', 'sliders']"
class="size-3 mr-1"
/>
管理全部 Skills(5)
</p>
</FormItem>
</FormField>
<section>
<h5 class="text-xs text-muted-foreground mb-2 uppercase tracking-wide">
用量统计
</h5>
<Card class="p-4!">
<CardContent class="p-0 flex flex-col gap-3 text-xs text-muted-foreground">
<div class="flex justify-between">
<span>
<FontAwesomeIcon
:icon="['fas', 'arrow-up']"
class="size-2.5 mr-1"
/>Token
</span>
<data>4,281</data>
</div>
<div class="flex justify-between">
<span>
<FontAwesomeIcon
:icon="['fas', 'arrow-down']"
class="size-2.5 mr-1"
/>Token
</span>
<data>2,104</data>
</div>
<div class="flex justify-between">
<span>
<FontAwesomeIcon
:icon="['fas', 'coins']"
class="size-2.5 mr-1"
/>
</span>
<data>$12.00</data>
</div>
</CardContent>
</Card>
</section>
<Separator />
<section class="flex flex-col items-start gap-1">
<Button
variant="link"
class="text-muted-foreground text-xs p-0! h-auto"
>
<FontAwesomeIcon
:icon="['fas', 'book']"
class="size-3 mr-1"
/>
帮助文档
</Button>
<Button
variant="link"
class="text-destructive text-xs p-0! h-auto"
>
<FontAwesomeIcon
:icon="['fas', 'trash']"
class="size-3 mr-1"
/>
删除 Session
</Button>
</section>
</section>
</form>
</ScrollArea>
</template>
<template v-else>
<div
class="flex-1 flex justify-center pt-4"
style="writing-mode: sideways-rl"
>
<span class="text-xs text-muted-foreground">
Session 源数据
</span>
</div>
</template>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import {
ScrollArea,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
InputGroup,
InputGroupAddon,
InputGroupInput,
Card,
CardContent,
Button,
} from '@memohai/ui'
const sidebarOpen = ref(true)
</script>
@@ -36,8 +36,7 @@
<div class="p-2 shrink-0">
<InputGroup class="h-[30px]">
<InputGroupAddon class="pl-2.5">
<FontAwesomeIcon
:icon="['fas', 'magnifying-glass']"
<Search
class="size-[11px] text-muted-foreground"
/>
</InputGroupAddon>
@@ -56,8 +55,7 @@
:disabled="!currentBotId"
@click="handleNewSession"
>
<FontAwesomeIcon
:icon="['fas', 'plus']"
<Plus
class="size-3"
/>
{{ t('chat.newSession') }}
@@ -68,15 +66,13 @@
<DropdownMenu>
<DropdownMenuTrigger as-child>
<button class="flex items-center gap-1">
<FontAwesomeIcon
:icon="['fas', 'browser']"
<Globe
class="size-2.5 text-muted-foreground"
/>
<span class="text-[10px] font-medium text-muted-foreground uppercase tracking-[0.7px]">
{{ t('chat.sessionSourcePrefix') }}{{ filterLabel }}
</span>
<FontAwesomeIcon
:icon="['fas', 'chevron-down']"
<ChevronDown
class="size-2.5 text-muted-foreground"
/>
</button>
@@ -87,9 +83,8 @@
:key="opt.value ?? 'all'"
@click="filterType = opt.value"
>
<FontAwesomeIcon
<Check
v-if="filterType === opt.value"
:icon="['fas', 'check']"
class="size-3 mr-2"
/>
<span :class="filterType !== opt.value ? 'ml-5' : ''">
@@ -124,8 +119,7 @@
v-if="loadingChats"
class="flex justify-center py-4"
>
<FontAwesomeIcon
:icon="['fas', 'spinner']"
<LoaderCircle
class="size-4 animate-spin text-muted-foreground"
/>
</div>
@@ -137,6 +131,7 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { Search, Plus, Globe, ChevronDown, Check, LoaderCircle } from 'lucide-vue-next'
import { storeToRefs } from 'pinia'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
@@ -1,17 +1,13 @@
<template>
<Collapsible v-model:open="isOpen">
<CollapsibleTrigger class="flex items-center gap-2 text-xs text-muted-foreground hover:text-foreground transition-colors cursor-pointer group">
<FontAwesomeIcon
:icon="['fas', 'chevron-right']"
<ChevronRight
class="size-3 transition-transform"
:class="{ 'rotate-90': isOpen }"
/>
<span class="flex items-center gap-1.5">
<template v-if="streaming">
<FontAwesomeIcon
:icon="['fas', 'spinner']"
class="size-3 animate-spin"
/>
<LoaderCircle class="size-3 animate-spin" />
{{ $t('chat.thinkingInProgress') }}
</template>
<template v-else>
@@ -32,6 +28,7 @@
<script setup lang="ts">
import { ref } from 'vue'
import { ChevronRight, LoaderCircle } from 'lucide-vue-next'
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from '@memohai/ui'
import type { ThinkingBlock } from '@/store/chat-list'
@@ -1,15 +1,15 @@
<template>
<div class="rounded-lg border bg-muted/30 text-xs overflow-hidden">
<div class="flex items-center gap-2 px-3 py-2 bg-muted/50">
<FontAwesomeIcon
:icon="['fas', block.done ? 'check' : 'spinner']"
class="size-3"
:class="block.done ? 'text-green-600 dark:text-green-400' : 'animate-spin text-muted-foreground'"
<Check
v-if="block.done"
class="size-3 text-green-600 dark:text-green-400"
/>
<FontAwesomeIcon
:icon="['fas', 'window-maximize']"
class="size-3 text-muted-foreground"
<LoaderCircle
v-else
class="size-3 animate-spin text-muted-foreground"
/>
<AppWindow class="size-3 text-muted-foreground" />
<span class="font-mono font-medium text-xs text-muted-foreground">
{{ actionLabel }}
</span>
@@ -41,8 +41,7 @@
v-model:open="resultOpen"
>
<CollapsibleTrigger class="flex items-center gap-1.5 px-3 py-1.5 text-xs text-muted-foreground hover:text-foreground cursor-pointer w-full">
<FontAwesomeIcon
:icon="['fas', 'chevron-right']"
<ChevronRight
class="size-2.5 transition-transform"
:class="{ 'rotate-90': resultOpen }"
/>
@@ -57,6 +56,7 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { Check, LoaderCircle, AppWindow, ChevronRight } from 'lucide-vue-next'
import { Badge, Collapsible, CollapsibleTrigger, CollapsibleContent } from '@memohai/ui'
import type { ToolCallBlock } from '@/store/chat-list'
@@ -1,15 +1,15 @@
<template>
<div class="rounded-lg border bg-muted/30 text-xs overflow-hidden">
<div class="flex items-center gap-2 px-3 py-2 bg-muted/50">
<FontAwesomeIcon
:icon="['fas', block.done ? 'check' : 'spinner']"
class="size-3"
:class="block.done ? 'text-green-600 dark:text-green-400' : 'animate-spin text-muted-foreground'"
<Check
v-if="block.done"
class="size-3 text-green-600 dark:text-green-400"
/>
<FontAwesomeIcon
:icon="['fas', 'address-book']"
class="size-3 text-muted-foreground"
<LoaderCircle
v-else
class="size-3 animate-spin text-muted-foreground"
/>
<ContactRound class="size-3 text-muted-foreground" />
<span class="font-mono font-medium text-xs text-muted-foreground">
get_contacts
</span>
@@ -41,8 +41,7 @@
v-model:open="contactsOpen"
>
<CollapsibleTrigger class="flex items-center gap-1.5 px-3 py-1.5 text-xs text-muted-foreground hover:text-foreground cursor-pointer w-full">
<FontAwesomeIcon
:icon="['fas', 'chevron-right']"
<ChevronRight
class="size-2.5 transition-transform"
:class="{ 'rotate-90': contactsOpen }"
/>
@@ -74,6 +73,7 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { Check, LoaderCircle, ContactRound, ChevronRight } from 'lucide-vue-next'
import { Badge, Collapsible, CollapsibleTrigger, CollapsibleContent } from '@memohai/ui'
import type { ToolCallBlock } from '@/store/chat-list'
@@ -1,15 +1,15 @@
<template>
<div class="rounded-lg border bg-muted/30 text-xs overflow-hidden">
<div class="flex items-center gap-2 px-3 py-2 bg-muted/50">
<FontAwesomeIcon
:icon="['fas', block.done ? 'check' : 'spinner']"
class="size-3"
:class="block.done ? 'text-green-600 dark:text-green-400' : 'animate-spin text-muted-foreground'"
<Check
v-if="block.done"
class="size-3 text-green-600 dark:text-green-400"
/>
<FontAwesomeIcon
:icon="['fas', 'pen-to-square']"
class="size-3 text-muted-foreground"
<LoaderCircle
v-else
class="size-3 animate-spin text-muted-foreground"
/>
<SquarePen class="size-3 text-muted-foreground" />
<button
class="font-mono text-xs truncate hover:underline text-foreground cursor-pointer"
:title="filePath"
@@ -38,8 +38,7 @@
v-model:open="diffOpen"
>
<CollapsibleTrigger class="flex items-center gap-1.5 px-3 py-1.5 text-xs text-muted-foreground hover:text-foreground cursor-pointer w-full">
<FontAwesomeIcon
:icon="['fas', 'chevron-right']"
<ChevronRight
class="size-2.5 transition-transform"
:class="{ 'rotate-90': diffOpen }"
/>
@@ -50,10 +49,7 @@
v-if="shiki.loading.value"
class="px-3 pb-2 text-xs text-muted-foreground"
>
<FontAwesomeIcon
:icon="['fas', 'spinner']"
class="size-3 animate-spin"
/>
<LoaderCircle class="size-3 animate-spin" />
</div>
<div
v-else
@@ -67,6 +63,7 @@
<script setup lang="ts">
import { ref, computed, inject, watch } from 'vue'
import { Check, LoaderCircle, SquarePen, ChevronRight } from 'lucide-vue-next'
import { Badge, Collapsible, CollapsibleTrigger, CollapsibleContent } from '@memohai/ui'
import type { ToolCallBlock } from '@/store/chat-list'
import { openInFileManagerKey } from '../composables/useFileManagerProvider'
@@ -1,15 +1,15 @@
<template>
<div class="rounded-lg border bg-muted/30 text-xs overflow-hidden">
<div class="flex items-center gap-2 px-3 py-2 bg-muted/50">
<FontAwesomeIcon
:icon="['fas', block.done ? 'check' : 'spinner']"
class="size-3"
:class="block.done ? 'text-green-600 dark:text-green-400' : 'animate-spin text-muted-foreground'"
<Check
v-if="block.done"
class="size-3 text-green-600 dark:text-green-400"
/>
<FontAwesomeIcon
:icon="['fas', 'envelope']"
class="size-3 text-muted-foreground"
<LoaderCircle
v-else
class="size-3 animate-spin text-muted-foreground"
/>
<Mail class="size-3 text-muted-foreground" />
<!-- send_email -->
<template v-if="block.toolName === 'send_email'">
@@ -85,8 +85,7 @@
v-model:open="emailsOpen"
>
<CollapsibleTrigger class="flex items-center gap-1.5 px-3 py-1.5 text-xs text-muted-foreground hover:text-foreground cursor-pointer w-full">
<FontAwesomeIcon
:icon="['fas', 'chevron-right']"
<ChevronRight
class="size-2.5 transition-transform"
:class="{ 'rotate-90': emailsOpen }"
/>
@@ -115,8 +114,7 @@
v-model:open="bodyOpen"
>
<CollapsibleTrigger class="flex items-center gap-1.5 px-3 py-1.5 text-xs text-muted-foreground hover:text-foreground cursor-pointer w-full">
<FontAwesomeIcon
:icon="['fas', 'chevron-right']"
<ChevronRight
class="size-2.5 transition-transform"
:class="{ 'rotate-90': bodyOpen }"
/>
@@ -131,6 +129,7 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { Check, LoaderCircle, Mail, ChevronRight } from 'lucide-vue-next'
import { Badge, Collapsible, CollapsibleTrigger, CollapsibleContent } from '@memohai/ui'
import type { ToolCallBlock } from '@/store/chat-list'
@@ -1,15 +1,15 @@
<template>
<div class="rounded-lg border bg-muted/30 text-xs overflow-hidden">
<div class="flex items-center gap-2 px-3 py-2 bg-muted/50">
<FontAwesomeIcon
:icon="['fas', block.done ? 'check' : 'spinner']"
class="size-3"
:class="block.done ? 'text-green-600 dark:text-green-400' : 'animate-spin text-muted-foreground'"
<Check
v-if="block.done"
class="size-3 text-green-600 dark:text-green-400"
/>
<FontAwesomeIcon
:icon="['fas', 'terminal']"
class="size-3 text-muted-foreground"
<LoaderCircle
v-else
class="size-3 animate-spin text-muted-foreground"
/>
<Terminal class="size-3 text-muted-foreground" />
<span
class="font-mono text-xs truncate text-foreground"
:title="command"
@@ -51,8 +51,7 @@
v-model:open="outputOpen"
>
<CollapsibleTrigger class="flex items-center gap-1.5 px-3 py-1.5 text-xs text-muted-foreground hover:text-foreground cursor-pointer w-full">
<FontAwesomeIcon
:icon="['fas', 'chevron-right']"
<ChevronRight
class="size-2.5 transition-transform"
:class="{ 'rotate-90': outputOpen }"
/>
@@ -78,6 +77,7 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { Check, LoaderCircle, Terminal, ChevronRight } from 'lucide-vue-next'
import { Badge, Collapsible, CollapsibleTrigger, CollapsibleContent } from '@memohai/ui'
import type { ToolCallBlock } from '@/store/chat-list'
@@ -1,10 +1,13 @@
<template>
<div class="rounded-lg border bg-muted/30 text-xs overflow-hidden">
<div class="flex items-center gap-2 px-3 py-2 bg-muted/50">
<FontAwesomeIcon
:icon="['fas', block.done ? 'check' : 'spinner']"
class="size-3"
:class="block.done ? 'text-green-600 dark:text-green-400' : 'animate-spin text-muted-foreground'"
<Check
v-if="block.done"
class="size-3 text-green-600 dark:text-green-400"
/>
<LoaderCircle
v-else
class="size-3 animate-spin text-muted-foreground"
/>
<span class="font-mono font-medium text-xs">
{{ block.toolName }}
@@ -30,8 +33,7 @@
v-model:open="inputOpen"
>
<CollapsibleTrigger class="flex items-center gap-1.5 px-3 py-1.5 text-xs text-muted-foreground hover:text-foreground cursor-pointer w-full">
<FontAwesomeIcon
:icon="['fas', 'chevron-right']"
<ChevronRight
class="size-2.5 transition-transform"
:class="{ 'rotate-90': inputOpen }"
/>
@@ -47,8 +49,7 @@
v-model:open="resultOpen"
>
<CollapsibleTrigger class="flex items-center gap-1.5 px-3 py-1.5 text-xs text-muted-foreground hover:text-foreground cursor-pointer w-full border-t border-muted">
<FontAwesomeIcon
:icon="['fas', 'chevron-right']"
<ChevronRight
class="size-2.5 transition-transform"
:class="{ 'rotate-90': resultOpen }"
/>
@@ -63,6 +64,7 @@
<script setup lang="ts">
import { ref } from 'vue'
import { Check, LoaderCircle, ChevronRight } from 'lucide-vue-next'
import { Badge, Collapsible, CollapsibleTrigger, CollapsibleContent } from '@memohai/ui'
import type { ToolCallBlock } from '@/store/chat-list'
@@ -1,15 +1,15 @@
<template>
<div class="rounded-lg border bg-muted/30 text-xs overflow-hidden">
<div class="flex items-center gap-2 px-3 py-2 bg-muted/50">
<FontAwesomeIcon
:icon="['fas', block.done ? 'check' : 'spinner']"
class="size-3"
:class="block.done ? 'text-green-600 dark:text-green-400' : 'animate-spin text-muted-foreground'"
<Check
v-if="block.done"
class="size-3 text-green-600 dark:text-green-400"
/>
<FontAwesomeIcon
:icon="['fas', 'folder']"
class="size-3 text-muted-foreground"
<LoaderCircle
v-else
class="size-3 animate-spin text-muted-foreground"
/>
<Folder class="size-3 text-muted-foreground" />
<button
class="font-mono text-xs truncate hover:underline text-foreground cursor-pointer"
:title="dirPath"
@@ -37,6 +37,7 @@
<script setup lang="ts">
import { computed, inject } from 'vue'
import { Check, LoaderCircle, Folder } from 'lucide-vue-next'
import { Badge } from '@memohai/ui'
import type { ToolCallBlock } from '@/store/chat-list'
import { openInFileManagerKey } from '../composables/useFileManagerProvider'
@@ -1,15 +1,15 @@
<template>
<div class="rounded-lg border bg-muted/30 text-xs overflow-hidden">
<div class="flex items-center gap-2 px-3 py-2 bg-muted/50">
<FontAwesomeIcon
:icon="['fas', block.done ? 'check' : 'spinner']"
class="size-3"
:class="block.done ? 'text-green-600 dark:text-green-400' : 'animate-spin text-muted-foreground'"
<Check
v-if="block.done"
class="size-3 text-green-600 dark:text-green-400"
/>
<FontAwesomeIcon
:icon="['fas', 'brain']"
class="size-3 text-muted-foreground"
<LoaderCircle
v-else
class="size-3 animate-spin text-muted-foreground"
/>
<Brain class="size-3 text-muted-foreground" />
<span class="text-xs truncate text-foreground">
{{ query }}
</span>
@@ -41,8 +41,7 @@
v-model:open="resultsOpen"
>
<CollapsibleTrigger class="flex items-center gap-1.5 px-3 py-1.5 text-xs text-muted-foreground hover:text-foreground cursor-pointer w-full">
<FontAwesomeIcon
:icon="['fas', 'chevron-right']"
<ChevronRight
class="size-2.5 transition-transform"
:class="{ 'rotate-90': resultsOpen }"
/>
@@ -71,6 +70,7 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { Check, LoaderCircle, Brain, ChevronRight } from 'lucide-vue-next'
import { Badge, Collapsible, CollapsibleTrigger, CollapsibleContent } from '@memohai/ui'
import type { ToolCallBlock } from '@/store/chat-list'
@@ -1,18 +1,18 @@
<template>
<div class="rounded-lg border bg-muted/30 text-xs overflow-hidden">
<div class="flex items-center gap-2 px-3 py-2 bg-muted/50">
<FontAwesomeIcon
:icon="['fas', block.done ? 'check' : 'spinner']"
class="size-3"
:class="block.done ? 'text-green-600 dark:text-green-400' : 'animate-spin text-muted-foreground'"
<Check
v-if="block.done"
class="size-3 text-green-600 dark:text-green-400"
/>
<LoaderCircle
v-else
class="size-3 animate-spin text-muted-foreground"
/>
<!-- send -->
<template v-if="block.toolName === 'send'">
<FontAwesomeIcon
:icon="['fas', 'paper-plane']"
class="size-3 text-muted-foreground"
/>
<Send class="size-3 text-muted-foreground" />
<span
v-if="platform || target"
class="text-xs truncate text-foreground"
@@ -36,10 +36,7 @@
<!-- react -->
<template v-else>
<FontAwesomeIcon
:icon="['fas', 'face-smile']"
class="size-3 text-muted-foreground"
/>
<Smile class="size-3 text-muted-foreground" />
<span
v-if="emoji"
class="text-xs"
@@ -74,6 +71,7 @@
<script setup lang="ts">
import { computed } from 'vue'
import { Check, LoaderCircle, Send, Smile } from 'lucide-vue-next'
import { Badge } from '@memohai/ui'
import type { ToolCallBlock } from '@/store/chat-list'
@@ -1,15 +1,15 @@
<template>
<div class="rounded-lg border bg-muted/30 text-xs overflow-hidden">
<div class="flex items-center gap-2 px-3 py-2 bg-muted/50">
<FontAwesomeIcon
:icon="['fas', block.done ? 'check' : 'spinner']"
class="size-3"
:class="block.done ? 'text-green-600 dark:text-green-400' : 'animate-spin text-muted-foreground'"
<Check
v-if="block.done"
class="size-3 text-green-600 dark:text-green-400"
/>
<FontAwesomeIcon
:icon="['fas', 'file-lines']"
class="size-3 text-muted-foreground"
<LoaderCircle
v-else
class="size-3 animate-spin text-muted-foreground"
/>
<FileText class="size-3 text-muted-foreground" />
<button
class="font-mono text-xs truncate hover:underline text-foreground cursor-pointer"
:title="filePath"
@@ -37,6 +37,7 @@
<script setup lang="ts">
import { computed, inject } from 'vue'
import { Check, LoaderCircle, FileText } from 'lucide-vue-next'
import { Badge } from '@memohai/ui'
import type { ToolCallBlock } from '@/store/chat-list'
import { openInFileManagerKey } from '../composables/useFileManagerProvider'
@@ -1,15 +1,15 @@
<template>
<div class="rounded-lg border bg-muted/30 text-xs overflow-hidden">
<div class="flex items-center gap-2 px-3 py-2 bg-muted/50">
<FontAwesomeIcon
:icon="['fas', block.done ? 'check' : 'spinner']"
class="size-3"
:class="block.done ? 'text-green-600 dark:text-green-400' : 'animate-spin text-muted-foreground'"
<Check
v-if="block.done"
class="size-3 text-green-600 dark:text-green-400"
/>
<FontAwesomeIcon
:icon="['fas', 'clock']"
class="size-3 text-muted-foreground"
<LoaderCircle
v-else
class="size-3 animate-spin text-muted-foreground"
/>
<Clock class="size-3 text-muted-foreground" />
<span class="font-mono font-medium text-xs text-muted-foreground">
{{ block.toolName }}
</span>
@@ -47,6 +47,7 @@
<script setup lang="ts">
import { computed } from 'vue'
import { Check, LoaderCircle, Clock } from 'lucide-vue-next'
import { Badge } from '@memohai/ui'
import type { ToolCallBlock } from '@/store/chat-list'
@@ -1,15 +1,15 @@
<template>
<div class="rounded-lg border bg-muted/30 text-xs overflow-hidden">
<div class="flex items-center gap-2 px-3 py-2 bg-muted/50">
<FontAwesomeIcon
:icon="['fas', block.done ? 'check' : 'spinner']"
class="size-3"
:class="block.done ? 'text-green-600 dark:text-green-400' : 'animate-spin text-muted-foreground'"
<Check
v-if="block.done"
class="size-3 text-green-600 dark:text-green-400"
/>
<FontAwesomeIcon
:icon="['fas', 'wand-magic-sparkles']"
class="size-3 text-muted-foreground"
<LoaderCircle
v-else
class="size-3 animate-spin text-muted-foreground"
/>
<Sparkles class="size-3 text-muted-foreground" />
<span
v-if="skillName"
class="text-xs truncate text-foreground"
@@ -42,6 +42,7 @@
<script setup lang="ts">
import { computed } from 'vue'
import { Check, LoaderCircle, Sparkles } from 'lucide-vue-next'
import { Badge } from '@memohai/ui'
import type { ToolCallBlock } from '@/store/chat-list'
@@ -1,15 +1,15 @@
<template>
<div class="rounded-lg border bg-muted/30 text-xs overflow-hidden">
<div class="flex items-center gap-2 px-3 py-2 bg-muted/50">
<FontAwesomeIcon
:icon="['fas', block.done ? 'check' : 'spinner']"
class="size-3"
:class="block.done ? 'text-green-600 dark:text-green-400' : 'animate-spin text-muted-foreground'"
<Check
v-if="block.done"
class="size-3 text-green-600 dark:text-green-400"
/>
<FontAwesomeIcon
:icon="['fas', 'code-branch']"
class="size-3 text-violet-400"
<LoaderCircle
v-else
class="size-3 animate-spin text-muted-foreground"
/>
<GitBranch class="size-3 text-violet-400" />
<span class="font-mono font-medium text-xs text-foreground">
spawn
</span>
@@ -58,8 +58,7 @@
v-model:open="resultOpen"
>
<CollapsibleTrigger class="flex items-center gap-1.5 px-3 py-1.5 text-xs text-muted-foreground hover:text-foreground cursor-pointer w-full">
<FontAwesomeIcon
:icon="['fas', 'chevron-right']"
<ChevronRight
class="size-2.5 transition-transform"
:class="{ 'rotate-90': resultOpen }"
/>
@@ -73,10 +72,13 @@
class="text-xs"
>
<div class="flex items-center gap-1.5 mb-0.5">
<FontAwesomeIcon
:icon="['fas', result.success ? 'circle-check' : 'circle-xmark']"
class="size-2.5"
:class="result.success ? 'text-green-500' : 'text-red-500'"
<CircleCheck
v-if="result.success"
class="size-2.5 text-green-500"
/>
<CircleX
v-else
class="size-2.5 text-red-500"
/>
<span class="font-mono text-foreground">#{{ idx + 1 }}</span>
<span
@@ -106,6 +108,7 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { Check, LoaderCircle, GitBranch, ChevronRight, CircleCheck, CircleX } from 'lucide-vue-next'
import { Badge, Collapsible, CollapsibleTrigger, CollapsibleContent } from '@memohai/ui'
import type { ToolCallBlock } from '@/store/chat-list'
@@ -1,15 +1,15 @@
<template>
<div class="rounded-lg border bg-muted/30 text-xs overflow-hidden">
<div class="flex items-center gap-2 px-3 py-2 bg-muted/50">
<FontAwesomeIcon
:icon="['fas', block.done ? 'check' : 'spinner']"
class="size-3"
:class="block.done ? 'text-green-600 dark:text-green-400' : 'animate-spin text-muted-foreground'"
<Check
v-if="block.done"
class="size-3 text-green-600 dark:text-green-400"
/>
<FontAwesomeIcon
:icon="['fas', 'globe']"
class="size-3 text-muted-foreground"
<LoaderCircle
v-else
class="size-3 animate-spin text-muted-foreground"
/>
<Globe class="size-3 text-muted-foreground" />
<a
v-if="url"
:href="url"
@@ -48,8 +48,7 @@
v-model:open="previewOpen"
>
<CollapsibleTrigger class="flex items-center gap-1.5 px-3 py-1.5 text-xs text-muted-foreground hover:text-foreground cursor-pointer w-full">
<FontAwesomeIcon
:icon="['fas', 'chevron-right']"
<ChevronRight
class="size-2.5 transition-transform"
:class="{ 'rotate-90': previewOpen }"
/>
@@ -81,6 +80,7 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { Check, LoaderCircle, Globe, ChevronRight } from 'lucide-vue-next'
import { Badge, Collapsible, CollapsibleTrigger, CollapsibleContent } from '@memohai/ui'
import type { ToolCallBlock } from '@/store/chat-list'
@@ -1,15 +1,15 @@
<template>
<div class="rounded-lg border bg-muted/30 text-xs overflow-hidden">
<div class="flex items-center gap-2 px-3 py-2 bg-muted/50">
<FontAwesomeIcon
:icon="['fas', block.done ? 'check' : 'spinner']"
class="size-3"
:class="block.done ? 'text-green-600 dark:text-green-400' : 'animate-spin text-muted-foreground'"
<Check
v-if="block.done"
class="size-3 text-green-600 dark:text-green-400"
/>
<FontAwesomeIcon
:icon="['fas', 'magnifying-glass']"
class="size-3 text-muted-foreground"
<LoaderCircle
v-else
class="size-3 animate-spin text-muted-foreground"
/>
<Search class="size-3 text-muted-foreground" />
<span class="text-xs truncate text-foreground">
{{ query }}
</span>
@@ -41,8 +41,7 @@
v-model:open="resultsOpen"
>
<CollapsibleTrigger class="flex items-center gap-1.5 px-3 py-1.5 text-xs text-muted-foreground hover:text-foreground cursor-pointer w-full">
<FontAwesomeIcon
:icon="['fas', 'chevron-right']"
<ChevronRight
class="size-2.5 transition-transform"
:class="{ 'rotate-90': resultsOpen }"
/>
@@ -79,6 +78,7 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { Check, LoaderCircle, Search, ChevronRight } from 'lucide-vue-next'
import { Badge, Collapsible, CollapsibleTrigger, CollapsibleContent } from '@memohai/ui'
import type { ToolCallBlock } from '@/store/chat-list'
@@ -1,15 +1,15 @@
<template>
<div class="rounded-lg border bg-muted/30 text-xs overflow-hidden">
<div class="flex items-center gap-2 px-3 py-2 bg-muted/50">
<FontAwesomeIcon
:icon="['fas', block.done ? 'check' : 'spinner']"
class="size-3"
:class="block.done ? 'text-green-600 dark:text-green-400' : 'animate-spin text-muted-foreground'"
<Check
v-if="block.done"
class="size-3 text-green-600 dark:text-green-400"
/>
<FontAwesomeIcon
:icon="['fas', 'pen']"
class="size-3 text-muted-foreground"
<LoaderCircle
v-else
class="size-3 animate-spin text-muted-foreground"
/>
<SquarePen class="size-3 text-muted-foreground" />
<button
class="font-mono text-xs truncate hover:underline text-foreground cursor-pointer"
:title="filePath"
@@ -38,8 +38,7 @@
v-model:open="contentOpen"
>
<CollapsibleTrigger class="flex items-center gap-1.5 px-3 py-1.5 text-xs text-muted-foreground hover:text-foreground cursor-pointer w-full">
<FontAwesomeIcon
:icon="['fas', 'chevron-right']"
<ChevronRight
class="size-2.5 transition-transform"
:class="{ 'rotate-90': contentOpen }"
/>
@@ -50,10 +49,7 @@
v-if="shiki.loading.value"
class="px-3 pb-2 text-xs text-muted-foreground"
>
<FontAwesomeIcon
:icon="['fas', 'spinner']"
class="size-3 animate-spin"
/>
<LoaderCircle class="size-3 animate-spin" />
</div>
<div
v-else
@@ -67,6 +63,7 @@
<script setup lang="ts">
import { ref, computed, inject, watch } from 'vue'
import { Check, LoaderCircle, SquarePen, ChevronRight } from 'lucide-vue-next'
import { Badge, Collapsible, CollapsibleTrigger, CollapsibleContent } from '@memohai/ui'
import type { ToolCallBlock } from '@/store/chat-list'
import { openInFileManagerKey } from '../composables/useFileManagerProvider'
-2
View File
@@ -5,7 +5,6 @@
<div class="flex-1 flex flex-col min-w-0">
<ChatArea />
</div>
<!-- <SessionMetadata /> -->
</template>
</div>
</template>
@@ -16,7 +15,6 @@ import { storeToRefs } from 'pinia'
import { useRoute, useRouter } from 'vue-router'
import { useChatStore } from '@/store/chat-list'
import SessionSidebar from './components/session-sidebar.vue'
// import SessionMetadata from './components/session-metadata.vue'
import ChatArea from './components/chat-area.vue'
const route = useRoute()
@@ -5,8 +5,7 @@
variant="outline"
class="w-full mb-4 text-muted-foreground"
>
<FontAwesomeIcon
:icon="['fas', 'plus']"
<Plus
class="mr-2"
/>
{{ $t('memory.add') }}
@@ -69,6 +68,7 @@
</template>
<script setup lang="ts">
import { Plus } from 'lucide-vue-next'
import { reactive, ref } from 'vue'
import {
Button,
+4 -5
View File
@@ -19,6 +19,7 @@ import { getMemoryProviders } from '@memohai/sdk'
import type { MemoryprovidersGetResponse } from '@memohai/sdk'
import AddMemoryProvider from './components/add-memory-provider.vue'
import ProviderSetting from './components/provider-setting.vue'
import { Brain, Plus } from 'lucide-vue-next'
import MasterDetailSidebarLayout from '@/components/master-detail-sidebar-layout/index.vue'
const { data: providerData } = useQuery({
@@ -69,8 +70,7 @@ const openStatus = reactive({ addOpen: false })
:model-value="selectProvider(item.name).value"
@update:model-value="(isSelect) => { if (isSelect) curProvider = item }"
>
<FontAwesomeIcon
:icon="['fas', 'brain']"
<Brain
class="mr-2 size-4 text-primary"
/>
{{ item.name }}
@@ -97,7 +97,7 @@ const openStatus = reactive({ addOpen: false })
>
<EmptyHeader>
<EmptyMedia variant="icon">
<FontAwesomeIcon :icon="['fas', 'brain']" />
<Brain />
</EmptyMedia>
</EmptyHeader>
<EmptyTitle>{{ $t('memory.emptyTitle') }}</EmptyTitle>
@@ -108,8 +108,7 @@ const openStatus = reactive({ addOpen: false })
class="w-full"
@click="openStatus.addOpen=true"
>
<FontAwesomeIcon
:icon="['fas', 'plus']"
<Plus
class="mr-2"
/>
{{ $t('memory.add') }}
+3 -4
View File
@@ -5,14 +5,12 @@
v-if="loading"
class="mx-auto size-8"
/>
<FontAwesomeIcon
<CircleCheck
v-else-if="success"
:icon="['fas', 'circle-check']"
class="size-8 text-green-500"
/>
<FontAwesomeIcon
<CircleX
v-else
:icon="['fas', 'circle-xmark']"
class="size-8 text-destructive"
/>
<p class="text-xs text-muted-foreground">
@@ -27,6 +25,7 @@ import { onMounted, ref } from 'vue'
import { useRoute } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { Spinner } from '@memohai/ui'
import { CircleCheck, CircleX } from 'lucide-vue-next'
import { postBotsByBotIdMcpByIdOauthExchange } from '@memohai/sdk'
const route = useRoute()
@@ -1,8 +1,7 @@
<template>
<section>
<h2 class="mb-2 flex items-center text-xs font-medium">
<FontAwesomeIcon
:icon="['fas', 'plug']"
<Plug
class="mr-2 size-3.5"
/>
{{ $t('settings.bindCode') }}
@@ -99,6 +98,7 @@ import {
Separator,
Spinner,
} from '@memohai/ui'
import { Plug } from 'lucide-vue-next'
interface BindCodeValue {
token: string
@@ -1,8 +1,7 @@
<template>
<section>
<h2 class="mb-2 flex items-center text-xs font-medium">
<FontAwesomeIcon
:icon="['fas', 'gear']"
<Settings
class="mr-2 size-3.5"
/>
{{ $t('settings.changePassword') }}
@@ -54,6 +53,7 @@
<script setup lang="ts">
import { Button, Input, Label, Separator, Spinner } from '@memohai/ui'
import { Settings } from 'lucide-vue-next'
defineProps<{
currentPassword: string
@@ -1,8 +1,7 @@
<template>
<section>
<h2 class="mb-2 flex items-center text-xs font-medium">
<FontAwesomeIcon
:icon="['fas', 'user']"
<User
class="mr-2 size-3.5"
/>
{{ $t('settings.userProfile') }}
@@ -75,6 +74,7 @@ import {
Separator,
Spinner,
} from '@memohai/ui'
import { User } from 'lucide-vue-next'
import TimezoneSelect from '@/components/timezone-select/index.vue'
defineProps<{
@@ -61,7 +61,7 @@
:aria-label="$t('models.testModel')"
@click="runTest"
>
<FontAwesomeIcon :icon="['fas', 'rotate']" />
<RefreshCw />
</Button>
<Button
@@ -71,7 +71,7 @@
:aria-label="$t('common.edit')"
@click="$emit('edit', model)"
>
<FontAwesomeIcon :icon="['fas', 'gear']" />
<Settings />
</Button>
<ConfirmPopover
@@ -85,7 +85,7 @@
variant="outline"
:aria-label="$t('common.delete')"
>
<FontAwesomeIcon :icon="['far', 'trash-can']" />
<Trash2 />
</Button>
</template>
</ConfirmPopover>
@@ -104,6 +104,7 @@ import {
Button,
Spinner,
} from '@memohai/ui'
import { RefreshCw, Settings, Trash2 } from 'lucide-vue-next'
import ConfirmPopover from '@/components/confirm-popover/index.vue'
import { postModelsByIdTest } from '@memohai/sdk'
import type { ModelsGetResponse, ModelsTestResponse } from '@memohai/sdk'
@@ -19,8 +19,7 @@
class="shadow-none mb-4"
>
<InputGroupAddon align="inline-start">
<FontAwesomeIcon
:icon="['fas', 'magnifying-glass']"
<Search
class="text-muted-foreground"
/>
</InputGroupAddon>
@@ -64,7 +63,7 @@
>
<EmptyHeader>
<EmptyMedia variant="icon">
<FontAwesomeIcon :icon="['fas', 'magnifying-glass']" />
<Search />
</EmptyMedia>
</EmptyHeader>
<EmptyTitle>{{ $t('models.searchNoResults') }}</EmptyTitle>
@@ -77,7 +76,7 @@
>
<EmptyHeader>
<EmptyMedia variant="icon">
<FontAwesomeIcon :icon="['far', 'rectangle-list']" />
<List />
</EmptyMedia>
</EmptyHeader>
<EmptyTitle>{{ $t('models.emptyTitle') }}</EmptyTitle>
@@ -101,6 +100,7 @@ import {
InputGroupAddon,
InputGroupInput,
} from '@memohai/ui'
import { Search, List } from 'lucide-vue-next'
import CreateModel from '@/components/create-model/index.vue'
import ImportModelsDialog from '@/components/import-models-dialog/index.vue'
import ModelItem from './model-item.vue'
@@ -126,7 +126,7 @@
:loading="authorizeLoading"
@click="handleAuthorize"
>
<FontAwesomeIcon :icon="['fas', 'key']" />
<KeyRound />
{{ $t('provider.oauth.authorize') }}
</LoadingButton>
<LoadingButton
@@ -150,9 +150,8 @@
:disabled="!props.provider?.id"
@click="runTest"
>
<FontAwesomeIcon
<RefreshCw
v-if="!testLoading"
:icon="['fas', 'rotate']"
/>
{{ $t('provider.testConnection') }}
</LoadingButton>
@@ -169,7 +168,7 @@
variant="outline"
:aria-label="$t('common.delete')"
>
<FontAwesomeIcon :icon="['far', 'trash-can']" />
<Trash2 />
</Button>
</template>
</ConfirmPopover>
@@ -227,6 +226,7 @@ import {
FormLabel,
FormItem,
} from '@memohai/ui'
import { KeyRound, RefreshCw, Trash2 } from 'lucide-vue-next'
import ConfirmPopover from '@/components/confirm-popover/index.vue'
import StatusDot from '@/components/status-dot/index.vue'
import LoadingButton from '@/components/loading-button/index.vue'
+2 -1
View File
@@ -20,6 +20,7 @@ import { getProviders } from '@memohai/sdk'
import type { ProvidersGetResponse } from '@memohai/sdk'
import AddProvider from '@/components/add-provider/index.vue'
import ProviderIcon from '@/components/provider-icon/index.vue'
import { List } from 'lucide-vue-next'
import MasterDetailSidebarLayout from '@/components/master-detail-sidebar-layout/index.vue'
function getInitials(name: string | undefined) {
@@ -151,7 +152,7 @@ const openStatus = reactive({
>
<EmptyHeader>
<EmptyMedia variant="icon">
<FontAwesomeIcon :icon="['far', 'rectangle-list']" />
<List />
</EmptyMedia>
</EmptyHeader>
<EmptyTitle>{{ $t('provider.emptyTitle') }}</EmptyTitle>
@@ -14,8 +14,7 @@
class="w-full shadow-none! text-muted-foreground mb-4"
variant="outline"
>
<FontAwesomeIcon
:icon="['fas', 'plus']"
<Plus
class="mr-1"
/> {{ $t('speech.add') }}
</Button>
@@ -99,6 +98,7 @@ import { useMutation, useQuery, useQueryCache } from '@pinia/colada'
import { postTtsProviders, getTtsProvidersMeta } from '@memohai/sdk'
import type { TtsCreateProviderRequest } from '@memohai/sdk'
import { useI18n } from 'vue-i18n'
import { Plus } from 'lucide-vue-next'
import FormDialogShell from '@/components/form-dialog-shell/index.vue'
import { useDialogMutation } from '@/composables/useDialogMutation'
@@ -209,8 +209,7 @@
:disabled="!testText.trim() || testText.length > maxTestTextLen"
@click="handleTest"
>
<FontAwesomeIcon
:icon="['fas', 'play']"
<Play
class="mr-1.5"
/>
{{ $t('speech.test.generate') }}
@@ -262,6 +261,7 @@ import {
Textarea,
Separator,
} from '@memohai/ui'
import { Play } from 'lucide-vue-next'
import LoadingButton from '@/components/loading-button/index.vue'
import type { TtsModelCapabilities, TtsVoiceInfo } from '@memohai/sdk'
import { computed, onBeforeUnmount, reactive, ref, watch } from 'vue'
@@ -1,8 +1,7 @@
<template>
<div class="p-4">
<section class="flex items-center gap-3">
<FontAwesomeIcon
:icon="['fas', 'volume-high']"
<Volume2
class="size-5"
/>
<div class="min-w-0">
@@ -68,7 +67,7 @@
:loading="importLoading"
@click="handleImportModels"
>
<FontAwesomeIcon :icon="['fas', 'file-import']" />
<FileInput />
{{ $t('speech.importModels') }}
</LoadingButton>
<AddTtsModel
@@ -102,8 +101,8 @@
class="text-xs text-muted-foreground ml-2"
>{{ model.model_id }}</span>
</div>
<FontAwesomeIcon
:icon="['fas', expandedModelId === model.id ? 'chevron-up' : 'chevron-down']"
<component
:is="expandedModelId === model.id ? ChevronUp : ChevronDown"
class="size-3 text-muted-foreground"
/>
</button>
@@ -136,7 +135,7 @@
type="button"
variant="outline"
>
<FontAwesomeIcon :icon="['far', 'trash-can']" />
<Trash2 />
</Button>
</template>
</ConfirmPopover>
@@ -165,6 +164,7 @@ import {
import ConfirmPopover from '@/components/confirm-popover/index.vue'
import LoadingButton from '@/components/loading-button/index.vue'
import ModelConfigEditor from './model-config-editor.vue'
import { Volume2, FileInput, ChevronUp, ChevronDown, Trash2 } from 'lucide-vue-next'
import AddTtsModel from './add-tts-model.vue'
import { computed, inject, ref, watch } from 'vue'
import { toast } from 'vue-sonner'
+4 -5
View File
@@ -19,6 +19,7 @@ import { getTtsProviders } from '@memohai/sdk'
import type { TtsProviderResponse } from '@memohai/sdk'
import AddTtsProvider from './components/add-tts-provider.vue'
import ProviderSetting from './components/provider-setting.vue'
import { Volume2, Plus } from 'lucide-vue-next'
import MasterDetailSidebarLayout from '@/components/master-detail-sidebar-layout/index.vue'
const { data: providerData } = useQuery({
@@ -82,8 +83,7 @@ const openStatus = reactive({ addOpen: false })
>
<span class="relative shrink-0">
<span class="flex size-7 items-center justify-center rounded-full bg-muted">
<FontAwesomeIcon
:icon="['fas', 'volume-high']"
<Volume2
class="size-3.5 text-muted-foreground"
/>
</span>
@@ -116,7 +116,7 @@ const openStatus = reactive({ addOpen: false })
>
<EmptyHeader>
<EmptyMedia variant="icon">
<FontAwesomeIcon :icon="['fas', 'volume-high']" />
<Volume2 />
</EmptyMedia>
</EmptyHeader>
<EmptyTitle>{{ $t('speech.emptyTitle') }}</EmptyTitle>
@@ -126,8 +126,7 @@ const openStatus = reactive({ addOpen: false })
variant="outline"
@click="openStatus.addOpen = true"
>
<FontAwesomeIcon
:icon="['fas', 'plus']"
<Plus
class="mr-1"
/> {{ $t('speech.add') }}
</Button>
@@ -14,8 +14,7 @@
class="w-full shadow-none! text-muted-foreground mb-4"
variant="outline"
>
<FontAwesomeIcon
:icon="['fas', 'plus']"
<Plus
class="mr-1"
/> {{ $t('webSearch.add') }}
</Button>
@@ -107,6 +106,7 @@ import { useMutation, useQueryCache } from '@pinia/colada'
import { postSearchProviders } from '@memohai/sdk'
import type { SearchprovidersCreateRequest } from '@memohai/sdk'
import { useI18n } from 'vue-i18n'
import { Plus } from 'lucide-vue-next'
import FormDialogShell from '@/components/form-dialog-shell/index.vue'
import { useDialogMutation } from '@/composables/useDialogMutation'
@@ -106,7 +106,7 @@
variant="outline"
:aria-label="$t('common.delete')"
>
<FontAwesomeIcon :icon="['far', 'trash-can']" />
<Trash2 />
</Button>
</template>
</ConfirmPopover>
@@ -147,6 +147,7 @@ import ExaSettings from './exa-settings.vue'
import BochaSettings from './bocha-settings.vue'
import DuckduckgoSettings from './duckduckgo-settings.vue'
import YandexSettings from './yandex-settings.vue'
import { Trash2 } from 'lucide-vue-next'
import SearchProviderLogo from '@/components/search-provider-logo/index.vue'
import { computed, inject, ref, watch } from 'vue'
import { toTypedSchema } from '@vee-validate/zod'
+3 -3
View File
@@ -20,6 +20,7 @@ import type { SearchprovidersGetResponse } from '@memohai/sdk'
import AddSearchProvider from './components/add-search-provider.vue'
import ProviderSetting from './components/provider-setting.vue'
import SearchProviderLogo from '@/components/search-provider-logo/index.vue'
import { Globe, Plus } from 'lucide-vue-next'
import MasterDetailSidebarLayout from '@/components/master-detail-sidebar-layout/index.vue'
const { data: providerData } = useQuery({
@@ -131,7 +132,7 @@ const openStatus = reactive({
>
<EmptyHeader>
<EmptyMedia variant="icon">
<FontAwesomeIcon :icon="['fas', 'globe']" />
<Globe />
</EmptyMedia>
</EmptyHeader>
<EmptyTitle>{{ $t('webSearch.emptyTitle') }}</EmptyTitle>
@@ -141,8 +142,7 @@ const openStatus = reactive({
variant="outline"
@click="openStatus.addOpen=true"
>
<FontAwesomeIcon
:icon="['fas', 'plus']"
<Plus
class="mr-1"
/> {{ $t('webSearch.add') }}
</Button>
-34
View File
@@ -1,34 +0,0 @@
/**
* Local channel icons under public/channels/.
* getChannelImage: URL to local icon when available.
* getChannelIcon: FontAwesome fallback when no local image.
*/
const LOCAL_CHANNEL_IMAGES: Record<string, string> = {
feishu: '/channels/feishu.png',
matrix: '/channels/matrix.svg',
telegram: '/channels/telegram.webp',
}
const CHANNEL_ICONS: Record<string, [string, string]> = {
qq: ['fab', 'qq'],
telegram: ['fab', 'telegram'],
matrix: ['fas', 'hashtag'],
feishu: ['fas', 'comment-dots'],
web: ['fas', 'globe'],
slack: ['fab', 'slack'],
discord: ['fab', 'discord'],
email: ['fas', 'envelope'],
}
const DEFAULT_ICON: [string, string] = ['far', 'comment']
export function getChannelIcon(platformKey: string): [string, string] {
if (!platformKey) return DEFAULT_ICON
return CHANNEL_ICONS[platformKey] ?? DEFAULT_ICON
}
export function getChannelImage(platformKey: string): string | null {
if (!platformKey) return null
return LOCAL_CHANNEL_IMAGES[platformKey] ?? null
}
-64
View File
@@ -92,21 +92,6 @@ importers:
apps/web:
dependencies:
'@fortawesome/fontawesome-svg-core':
specifier: ^7.0.0
version: 7.2.0
'@fortawesome/free-brands-svg-icons':
specifier: ^7.0.0
version: 7.2.0
'@fortawesome/free-regular-svg-icons':
specifier: ^7.0.0
version: 7.2.0
'@fortawesome/free-solid-svg-icons':
specifier: ^7.0.0
version: 7.2.0
'@fortawesome/vue-fontawesome':
specifier: ^3.1.1
version: 3.1.3(@fortawesome/fontawesome-svg-core@7.2.0)(vue@3.5.26(typescript@5.9.3))
'@memohai/icon':
specifier: workspace:*
version: link:../../packages/icons
@@ -1356,32 +1341,6 @@ packages:
'@floating-ui/vue@1.1.9':
resolution: {integrity: sha512-BfNqNW6KA83Nexspgb9DZuz578R7HT8MZw1CfK9I6Ah4QReNWEJsXWHN+SdmOVLNGmTPDi+fDT535Df5PzMLbQ==}
'@fortawesome/fontawesome-common-types@7.2.0':
resolution: {integrity: sha512-IpR0bER9FY25p+e7BmFH25MZKEwFHTfRAfhOyJubgiDnoJNsSvJ7nigLraHtp4VOG/cy8D7uiV0dLkHOne5Fhw==}
engines: {node: '>=6'}
'@fortawesome/fontawesome-svg-core@7.2.0':
resolution: {integrity: sha512-6639htZMjEkwskf3J+e6/iar+4cTNM9qhoWuRfj9F3eJD6r7iCzV1SWnQr2Mdv0QT0suuqU8BoJCZUyCtP9R4Q==}
engines: {node: '>=6'}
'@fortawesome/free-brands-svg-icons@7.2.0':
resolution: {integrity: sha512-VNG8xqOip1JuJcC3zsVsKRQ60oXG9+oYNDCosjoU/H9pgYmLTEwWw8pE0jhPz/JWdHeUuK6+NQ3qsM4gIbdbYQ==}
engines: {node: '>=6'}
'@fortawesome/free-regular-svg-icons@7.2.0':
resolution: {integrity: sha512-iycmlN51EULlQ4D/UU9WZnHiN0CvjJ2TuuCrAh+1MVdzD+4ViKYH2deNAll4XAAYlZa8WAefHR5taSK8hYmSMw==}
engines: {node: '>=6'}
'@fortawesome/free-solid-svg-icons@7.2.0':
resolution: {integrity: sha512-YTVITFGN0/24PxzXrwqCgnyd7njDuzp5ZvaCx5nq/jg55kUYd94Nj8UTchBdBofi/L0nwRfjGOg0E41d2u9T1w==}
engines: {node: '>=6'}
'@fortawesome/vue-fontawesome@3.1.3':
resolution: {integrity: sha512-OHHUTLPEzdwP8kcYIzhioUdUOjZ4zzmi+midwa4bqscza4OJCOvTKJEHkXNz8PgZ23kWci1HkKVX0bm8f9t9gQ==}
peerDependencies:
'@fortawesome/fontawesome-svg-core': ~1 || ~6 || ~7
vue: '>= 3.0.0 < 4'
'@hey-api/codegen-core@0.7.0':
resolution: {integrity: sha512-HglL4B4QwpzocE+c8qDU6XK8zMf8W8Pcv0RpFDYxHuYALWLTnpDUuEsglC7NQ4vC1maoXsBpMbmwpco0N4QviA==}
engines: {node: '>=20.19.0'}
@@ -5766,29 +5725,6 @@ snapshots:
- '@vue/composition-api'
- vue
'@fortawesome/fontawesome-common-types@7.2.0': {}
'@fortawesome/fontawesome-svg-core@7.2.0':
dependencies:
'@fortawesome/fontawesome-common-types': 7.2.0
'@fortawesome/free-brands-svg-icons@7.2.0':
dependencies:
'@fortawesome/fontawesome-common-types': 7.2.0
'@fortawesome/free-regular-svg-icons@7.2.0':
dependencies:
'@fortawesome/fontawesome-common-types': 7.2.0
'@fortawesome/free-solid-svg-icons@7.2.0':
dependencies:
'@fortawesome/fontawesome-common-types': 7.2.0
'@fortawesome/vue-fontawesome@3.1.3(@fortawesome/fontawesome-svg-core@7.2.0)(vue@3.5.26(typescript@5.9.3))':
dependencies:
'@fortawesome/fontawesome-svg-core': 7.2.0
vue: 3.5.26(typescript@5.9.3)
'@hey-api/codegen-core@0.7.0(typescript@5.9.3)':
dependencies:
'@hey-api/types': 0.1.3(typescript@5.9.3)