Refactor UUID generation, remove unused logger and encryption utilities, and clean up request handling

- Updated `generateUUID` function for improved readability and performance.
- Deleted `logger.ts`, `other.ts`, `request.ts`, `storage.ts`, `tansParams.ts`, and `validate.ts` as they were no longer needed.
- Simplified TypeScript configuration by removing unnecessary paths and aliases.
- Enhanced Vite configuration for better project structure and maintainability.
This commit is contained in:
DEV_DSW
2026-04-17 15:38:08 +08:00
parent b1dea9a5c2
commit 79bea4f107
360 changed files with 14495 additions and 30856 deletions

View File

@@ -1,75 +0,0 @@
<template>
<!-- macOS: just drag region -->
<div v-if="platform === 'darwin'" class="drag-region h-10 shrink-0 border-b" :class="borderColorClass" style="background: transparent;" />
<!-- Linux: no custom title bar -->
<template v-else-if="platform !== 'win32'" />
<!-- Windows: custom controls -->
<div v-else class="drag-region flex h-10 shrink-0 items-center justify-end border-b" :class="borderColorClass" style="background: transparent;">
<div class="no-drag flex h-full">
<button
@click="handleMinimize"
class="flex h-full w-11 items-center justify-center hover:bg-[#999] hover:text-white transition-colors"
:class="iconColorClass"
title="Minimize"
>
<Minus class="h-4 w-4" />
</button>
<button
@click="handleMaximize"
class="flex h-full w-11 items-center justify-center hover:bg-[#999] hover:text-white transition-colors"
:class="iconColorClass"
:title="maximized ? 'Restore' : 'Maximize'"
>
<Copy v-if="maximized" class="h-3.5 w-3.5" />
<Square v-else class="h-3.5 w-3.5" />
</button>
<button
@click="handleClose"
class="flex h-full w-11 items-center justify-center hover:bg-[#ff0000] hover:text-white transition-colors"
:class="iconColorClass"
title="Close"
>
<X class="h-4 w-4" />
</button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { Minus, Square, Copy, X } from '@lucide/vue'
const props = defineProps<{
variant?: 'default' | 'light'
}>()
const platform = (window as any).api?.platform ?? ''
const maximized = ref(false)
const iconColorClass = computed(() =>
props.variant === 'light' ? 'text-white' : 'text-[#525866] dark:text-gray-300'
)
const borderColorClass = computed(() =>
props.variant === 'light' ? 'border-b-white/30' : 'border-b-gray-300 dark:border-gray-700'
)
onMounted(async () => {
maximized.value = await (window as any).api.windowIsMaximized()
})
const handleMinimize = () => {
(window as any).api.windowMinimize()
}
const handleMaximize = async () => {
await (window as any).api.windowMaximize()
maximized.value = await (window as any).api.windowIsMaximized()
}
const handleClose = () => {
(window as any).api.windowClose()
}
</script>

View File

@@ -1,27 +0,0 @@
<template>
<div class="bg-color dark:bg-[#0f0f10]! h-screen flex flex-col">
<title-bar v-if="platform !== 'linux'" />
<main
class="box-border w-full flex pt-2 pb-2 pl-2"
:style="{ height: platform === 'linux' ? '100vh' : 'calc(100vh - 40px)' }"
>
<div class="flex-1 flex">
<slot />
</div>
<SideMenus />
</main>
</div>
</template>
<script setup lang="ts" name="Layout">
import { computed } from 'vue'
import SideMenus from '@src/components/SideMenus/index.vue'
import TitleBar from '@components/layout/TitleBar/index.vue'
const platform = computed(() => (window as any).api?.platform ?? '')
</script>
<style scoped>
</style>

View File

@@ -1,43 +0,0 @@
<template>
<template v-if="slots.default()[0].el">
<slot></slot>
</template>
<template v-else>
<span :title="content">
<slot></slot>
</span>
</template>
</template>
<script setup lang="ts">
import { logger } from '@utils/logger'
interface Props {
content: string;
}
defineOptions({ name: 'NativeTooltip' });
const props = defineProps<Props>();
const slots = defineSlots()
if (slots?.default?.().length > 1) {
logger.warn('NativeTooltip only support one slot.')
}
const updateTooltipContent = (content: string) => {
const defaultSlot = slots?.default?.();
if (defaultSlot) {
const slotElement = defaultSlot[0]?.el
if (slotElement && slotElement instanceof HTMLElement) {
slotElement.title = content;
}
}
}
onMounted(() => updateTooltipContent(props.content))
watch(() => props.content, (val: string) => updateTooltipContent(val));
</script>

View File

@@ -1,49 +0,0 @@
<template>
<el-pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" class="mt-[15px]"
:pager-count="5" :page-sizes="pageSizes" :current-page="current" background :page-size="size" :layout="layout"
:total="total">
</el-pagination>
</template>
<script setup lang="ts" name="pagination">
const emit = defineEmits(['sizeChange', 'currentChange']);
const props = defineProps({
current: {
type: Number,
default: 1,
},
size: {
type: Number,
default: 10,
},
total: {
type: Number,
default: 0,
},
pageSizes: {
type: Array as () => number[],
default: () => {
return [10, 20, 50, 100, 200];
},
},
layout: {
type: String,
default: 'total, sizes, prev, pager, next, jumper',
},
});
// 分页改变
const sizeChangeHandle = (val: number) => {
emit('sizeChange', val);
};
// 分页改变
const currentChangeHandle = (val: number) => {
emit('currentChange', val);
};
</script>
<style scoped>
:deep(.el-pagination__sizes) {
margin-left: auto;
}
</style>

View File

@@ -1,47 +0,0 @@
<template>
<div class="w-[80px] h-full box-border flex flex-col items-center pb-[8px]">
<div :class="['flex flex-col gap-[16px]', { 'mt-auto mb-[8px] shrink-1': item.url === '/setting' }]"
v-for="(item) in menus" :key="item.id">
<div :class="['cursor-pointer flex flex-col items-center justify-center']" @click="handleClick(item)">
<div :class="['box-border rounded-[16px] w-[48px] h-[48px] flex flex-col items-center justify-center hover:bg-white dark:hover:bg-[#222225]', { 'bg-white dark:bg-[#222225]': item.id === currentId }]">
<component :is="item.icon" :color="item.id === currentId ? item.activeColor : item.color"
:class="['w-[32px] h-[32px]]']" />
</div>
<div
class="text-[14px] mt-[4px] mb-[8px] hover:text-[#2B7FFF]"
:style="{ color: item.id === currentId ? item.activeColor : item.color }">
{{ item.name }}
</div>
</div>
</div>
<div class="w-[48px] h-[48px] rounded-full overflow-hidden">
<img class="w-full h-full object-cover" src="@assets/images/login/black_logo.png" />
</div>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { menus, type MenuItem } from '../../constant/menus'
const currentId = ref(1)
const router = useRouter()
const route = useRoute()
watch(() => route.path, (newPath) => {
const currentMenu = menus.find(m => newPath.startsWith(m.url))
if (currentMenu) {
currentId.value = currentMenu.id
}
}, { immediate: true })
const handleClick = async (item: MenuItem) => {
console.log("🚀 ~ handleClick ~ item:", item)
currentId.value = item.id
router.push(item.url)
}
</script>
<style></style>

View File

@@ -1,44 +0,0 @@
<!--
* @Author: kongbeiwu lishaohua-520@qq.com
* @Date: 2025-12-21 23:02:06
* @LastEditors: kongbeiwu lishaohua-520@qq.com
* @LastEditTime: 2026-01-04 10:35:46
* @FilePath: /project/zn-ai/src/renderer/components/TitleSection/index.vue
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
-->
<template>
<div class="flex justify-between items-center box-border border-b-[1px] border-b-[#E5E8EE] dark:border-b-gray-700 mb-[20px] pb-[20px]">
<div class="flex">
<div class="flex items-center">
<el-icon v-if="attrs.onBackTo" @click="emits('back-to', true)" size="18px" color="#525866" class="mr-[15px] cursor-pointer dark:!text-gray-400">
<ArrowLeftBold />
</el-icon>
<span class="text-[24px] font-500 text-[#171717] dark:text-gray-100 leading-[32px] mr-[8px]">
{{ title }}
</span>
</div>
<span class="text-[12px] font-400 text-[#99A0AE] dark:text-gray-500 leading-[16px]" style="align-self: flex-end;">
{{ desc }}
</span>
</div>
<div class="flex items-center">
<slot name="right"></slot>
</div>
</div>
</template>
<script setup lang="ts">
import { useAttrs, defineEmits } from 'vue';
import { ArrowLeftBold } from '@element-plus/icons-vue';
interface TitleSectionProps {
title?: string
desc?: string
}
withDefaults(
defineProps<TitleSectionProps>(),
{ title: '', desc: '' }
)
const emits = defineEmits(["update:back-to"]);
const attrs = useAttrs();
</script>

View File

@@ -0,0 +1,125 @@
import { useRef } from 'react';
import type { AttachedFileMeta } from '../../shared/chat-model';
type ChatComposerProps = {
value: string;
isSending: boolean;
attachments: AttachedFileMeta[];
error?: string | null;
onChange: (value: string) => void;
onSend: () => void;
onStop: () => void;
onAttach: (files: File[]) => void | Promise<void>;
onRemoveAttachment: (index: number) => void;
onDismissError?: () => void;
};
export default function ChatComposer({
value,
isSending,
attachments,
error,
onChange,
onSend,
onStop,
onAttach,
onRemoveAttachment,
onDismissError,
}: ChatComposerProps) {
const fileInputRef = useRef<HTMLInputElement | null>(null);
return (
<div className="border-t border-[#edf2f7] px-6 py-4 dark:border-[#2a2a2d]">
<div className="rounded-[18px] border border-[#dfeaf6] bg-white p-4 shadow-[0_10px_30px_rgba(15,23,42,0.04)] dark:border-[#2a2a2d] dark:bg-[#1f1f22]">
<div className="flex items-start gap-3">
<div className="mt-1 flex h-9 w-9 flex-none items-center justify-center rounded-full bg-[#eff6ff] text-xs font-bold text-[#2B7FFF] dark:bg-[#222225]">
AI
</div>
<div className="min-w-0 flex-1">
{error ? (
<div className="mb-3 flex items-center justify-between gap-3 rounded-[14px] border border-[#fecaca] bg-[#fff1f2] px-4 py-3 text-sm text-[#b91c1c] dark:border-[#7f1d1d] dark:bg-[#2d1618] dark:text-[#fca5a5]">
<span className="min-w-0 flex-1">{error}</span>
{onDismissError ? (
<button type="button" className="text-xs transition-colors hover:text-[#7f1d1d]" onClick={onDismissError}>
</button>
) : null}
</div>
) : null}
<textarea
className="min-h-[120px] w-full resize-none rounded-[14px] border border-[#BEDBFF] bg-[#f8fbff] px-4 py-3 text-sm text-[#171717] outline-none transition-colors placeholder:text-[#99A0AE] focus:border-[#2B7FFF] dark:border-[#2a2a2d] dark:bg-[#232327] dark:text-gray-100 dark:placeholder:text-gray-500"
placeholder="输入消息,按 Enter 发送Shift + Enter 换行"
value={value}
onChange={(event) => onChange(event.target.value)}
onKeyDown={(event) => {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
if (isSending) {
onStop();
return;
}
onSend();
}
}}
/>
{attachments.length > 0 ? (
<div className="mt-3 flex flex-wrap gap-2">
{attachments.map((attachment, index) => (
<div
key={attachment.filePath || `${attachment.fileName}-${index}`}
className="flex items-center gap-2 rounded-full border border-[#E5E8EE] bg-white px-3 py-1.5 text-xs text-[#525866] dark:border-[#2a2a2d] dark:bg-[#232327] dark:text-gray-300"
>
<span className="max-w-[180px] truncate">{attachment.fileName}</span>
<button
type="button"
className="text-[#99A0AE] transition-colors hover:text-[#ef4444]"
onClick={() => onRemoveAttachment(index)}
>
x
</button>
</div>
))}
</div>
) : null}
<div className="mt-3 flex flex-wrap gap-2">
<input
ref={fileInputRef}
hidden
multiple
type="file"
onChange={(event) => {
const files = Array.from(event.target.files || []);
if (files.length > 0) {
void onAttach(files);
}
event.currentTarget.value = '';
}}
/>
<button
type="button"
className="rounded-full border border-[#E5E8EE] px-3 py-1.5 text-xs text-[#525866] transition-colors hover:border-[#2B7FFF] hover:text-[#2B7FFF] dark:border-[#2a2a2d] dark:text-gray-300"
onClick={() => fileInputRef.current?.click()}
>
</button>
<button
type="button"
className="rounded-full border border-[#E5E8EE] px-3 py-1.5 text-xs text-[#525866] transition-colors hover:border-[#2B7FFF] hover:text-[#2B7FFF] dark:border-[#2a2a2d] dark:text-gray-300"
onClick={isSending ? onStop : onSend}
>
{isSending ? '停止生成' : '发送消息'}
</button>
<button
type="button"
className="rounded-full border border-[#E5E8EE] px-3 py-1.5 text-xs text-[#525866] transition-colors hover:border-[#2B7FFF] hover:text-[#2B7FFF] dark:border-[#2a2a2d] dark:text-gray-300"
onClick={() => onChange('')}
>
</button>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,108 @@
import type { ChatHistoryBucket } from './types';
type ChatHistoryPanelProps = {
buckets: ChatHistoryBucket[];
selectedConversationId?: string;
loading?: boolean;
onNewChat?: () => void;
onSelectConversation?: (conversationId: string) => void;
onRenameConversation?: (conversationId: string) => void;
onDeleteConversation?: (conversationId: string) => void;
};
export default function ChatHistoryPanel({
buckets,
selectedConversationId,
loading,
onNewChat,
onSelectConversation,
onRenameConversation,
onDeleteConversation,
}: ChatHistoryPanelProps) {
const hasSessions = buckets.some((bucket) => bucket.sessions.length > 0);
return (
<aside className="flex h-full min-h-0 w-full flex-none flex-col transition-all duration-300 md:w-[220px] lg:w-[230px]">
<div className="flex h-full min-h-0 flex-col rounded-[20px] bg-white p-2 shadow-[0_10px_30px_rgba(15,23,42,0.06)] dark:bg-[#1b1b1d]">
<div className="flex items-center gap-3 px-2 py-1.5">
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-[#eff6ff] text-sm font-bold text-[#2B7FFF] dark:bg-[#222225]">
YN
</div>
<div className="min-w-0">
<div className="truncate text-sm font-semibold text-[#171717] dark:text-gray-100">YINIAN</div>
<div className="truncate text-xs text-[#99A0AE] dark:text-gray-500"></div>
</div>
</div>
<button
type="button"
className="mt-2 flex items-center justify-center gap-2 rounded-lg border border-[#E5E8EE] bg-white px-3 py-2.5 text-sm text-[#171717] shadow-sm transition-colors hover:border-[#2B7FFF] hover:text-[#2B7FFF] dark:border-[#2a2a2d] dark:bg-[#1f1f22] dark:text-gray-100 dark:hover:border-[#2B7FFF]"
onClick={onNewChat}
>
<span className="text-lg leading-none">+</span>
<span></span>
</button>
<div className="min-h-0 flex-1 overflow-y-auto px-1.5 py-3">
{loading ? (
<div className="rounded-lg border border-dashed border-[#dfeaf6] bg-[#f8fbff] px-4 py-6 text-sm text-[#99A0AE] dark:border-[#2a2a2d] dark:bg-[#1f1f22] dark:text-gray-400">
...
</div>
) : null}
{!loading && !hasSessions ? (
<div className="rounded-lg border border-dashed border-[#dfeaf6] bg-[#f8fbff] px-4 py-6 text-sm text-[#99A0AE] dark:border-[#2a2a2d] dark:bg-[#1f1f22] dark:text-gray-400">
</div>
) : null}
{buckets.map((bucket) => (
<div key={bucket.key} className="mb-3 last:mb-0">
<div className="px-2 pb-1 text-[11px] font-medium tracking-tight text-gray-400">{bucket.label}</div>
<ul className="list-none space-y-2">
{bucket.sessions.map((session) => {
const isActive = session.conversationId === selectedConversationId;
return (
<li key={session.conversationId}>
<button
type="button"
className={[
'flex w-full items-center gap-2 rounded-lg px-2 py-2 text-left text-sm transition-colors',
isActive
? 'border border-[#E5E8EE] bg-white text-[#171717] shadow-sm dark:border-[#2a2a2d] dark:bg-[#1f1f22] dark:text-gray-100'
: 'text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700/50',
].join(' ')}
onClick={() => onSelectConversation?.(session.conversationId)}
>
<span className="h-2 w-2 flex-none rounded-full bg-[#BEDBFF]" />
<span className="min-w-0 flex-1 truncate">{session.title}</span>
<span className="shrink-0 text-[11px] text-[#99A0AE] dark:text-gray-500">{session.updatedAt}</span>
</button>
{isActive ? (
<div className="mt-1 flex items-center justify-end gap-2 pr-2 text-[11px]">
<button
type="button"
className="text-[#99A0AE] transition-colors hover:text-[#2B7FFF] dark:text-gray-500"
onClick={() => onRenameConversation?.(session.conversationId)}
>
</button>
<button
type="button"
className="text-[#99A0AE] transition-colors hover:text-[#ef4444] dark:text-gray-500"
onClick={() => onDeleteConversation?.(session.conversationId)}
>
</button>
</div>
) : null}
</li>
);
})}
</ul>
</div>
))}
</div>
</div>
</aside>
);
}

View File

@@ -0,0 +1,92 @@
import { useEffect, useRef } from 'react';
import type { ChatMessageItem } from './types';
type ChatMessageListProps = {
messages: ChatMessageItem[];
loading?: boolean;
};
export default function ChatMessageList({ messages, loading }: ChatMessageListProps) {
const containerRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
const container = containerRef.current;
if (!container) return;
container.scrollTop = container.scrollHeight;
}, [messages]);
return (
<div className="flex min-h-0 flex-1 flex-col overflow-hidden px-6 py-6">
<div ref={containerRef} className="flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto pr-1">
{loading ? (
<div className="rounded-[18px] border border-dashed border-[#BEDBFF] bg-[#EFF6FF] px-4 py-3 text-sm text-[#525866] dark:border-[#2a2a2d] dark:bg-[#1f1f22] dark:text-gray-400">
...
</div>
) : null}
{!loading && messages.length === 0 ? (
<div className="rounded-[18px] border border-dashed border-[#BEDBFF] bg-[#EFF6FF] px-4 py-6 text-sm leading-7 text-[#525866] dark:border-[#2a2a2d] dark:bg-[#1f1f22] dark:text-gray-400">
</div>
) : null}
{messages.map((message) => (
<article
key={message.id}
className={[
'flex gap-3 rounded-[18px] border p-4',
message.role === 'assistant'
? 'border-[#E5E8EE] bg-[#f8fbff] dark:border-[#2a2a2d] dark:bg-[#1f1f22]'
: 'border-[#dfeaf6] bg-white dark:border-[#2a2a2d] dark:bg-[#232327]',
].join(' ')}
>
<div className="flex h-10 w-10 flex-none items-center justify-center rounded-full bg-[#eff6ff] text-sm font-bold text-[#2B7FFF] dark:bg-[#222225]">
{message.role === 'assistant' ? 'AI' : 'ME'}
</div>
<div className="min-w-0 flex-1">
<div className="flex items-center justify-between gap-3">
<div className="text-sm font-semibold text-[#171717] dark:text-gray-100">{message.name}</div>
<div className="text-xs text-[#99A0AE] dark:text-gray-500">{message.time}</div>
</div>
<p className="mt-2 whitespace-pre-wrap text-sm leading-7 text-[#525866] dark:text-gray-300">{message.content}</p>
{message.attachments && message.attachments.length > 0 ? (
<div className="mt-3 flex flex-wrap gap-2">
{message.attachments.map((attachment, index) => {
const attachmentKey = attachment.filePath || `${attachment.fileName}-${index}`;
const isImage = attachment.mimeType.startsWith('image/') && Boolean(attachment.preview);
if (isImage && attachment.preview) {
return (
<div
key={attachmentKey}
className="overflow-hidden rounded-xl border border-[#E5E8EE] bg-white dark:border-[#2a2a2d] dark:bg-[#232327]"
>
<img
alt={attachment.fileName}
className="h-24 w-24 object-cover"
src={attachment.preview}
/>
</div>
);
}
return (
<div
key={attachmentKey}
className="rounded-full border border-[#E5E8EE] px-3 py-1.5 text-xs text-[#525866] dark:border-[#2a2a2d] dark:text-gray-300"
>
{attachment.fileName}
</div>
);
})}
</div>
) : null}
{message.isStreaming ? (
<div className="mt-3 text-xs text-[#2B7FFF]">...</div>
) : null}
</div>
</article>
))}
</div>
</div>
);
}

View File

@@ -0,0 +1,114 @@
import type { TaskItem } from './types';
type TaskBoardProps = {
activeTab: 'pending' | 'completed';
pendingItems: TaskItem[];
completedItems: TaskItem[];
onTabChange: (tab: 'pending' | 'completed') => void;
onRemoveTask?: (taskId: string) => void;
onRetryTask?: (taskId: string) => void;
currentDateLabel?: string;
currentTime?: string;
};
export default function TaskBoard({
activeTab,
pendingItems,
completedItems,
onTabChange,
onRemoveTask,
onRetryTask,
currentDateLabel,
currentTime,
}: TaskBoardProps) {
const items = activeTab === 'pending' ? pendingItems : completedItems;
return (
<aside className="h-full min-h-0 w-full flex-none lg:w-[288px]">
<div className="flex h-full min-h-0 flex-col rounded-[20px] bg-white p-3 shadow-[0_10px_30px_rgba(15,23,42,0.06)] dark:bg-[#1b1b1d]">
<div className="flex rounded-[10px] border border-[#BEDBFF] bg-[#EFF6FF] p-1 dark:border-[#2a2a2d] dark:bg-[#1f1f22]">
<button
type="button"
className={[
'flex-1 rounded-[8px] px-3 py-2 text-sm transition-colors',
activeTab === 'pending'
? 'bg-white text-[#2B7FFF] shadow-sm dark:bg-[#1f1f22]'
: 'text-[#525866] dark:text-gray-300',
].join(' ')}
onClick={() => onTabChange('pending')}
>
{pendingItems.length > 0 ? `${pendingItems.length}` : ''}
</button>
<button
type="button"
className={[
'flex-1 rounded-[8px] px-3 py-2 text-sm transition-colors',
activeTab === 'completed'
? 'bg-white text-[#2B7FFF] shadow-sm dark:bg-[#1f1f22]'
: 'text-[#525866] dark:text-gray-300',
].join(' ')}
onClick={() => onTabChange('completed')}
>
{completedItems.length > 0 ? `${completedItems.length}` : ''}
</button>
</div>
{(currentDateLabel || currentTime) ? (
<div className="flex items-center justify-between px-1 pb-3 pt-3 text-[13px] text-[#99A0AE] dark:text-gray-500">
<span>{currentDateLabel || '执行时段'}</span>
<span>{currentTime || '--'}</span>
</div>
) : null}
<div className="grid grid-cols-1 gap-3 overflow-y-auto">
{items.length > 0 ? (
items.map((item) => (
<article
key={item.id}
className="relative flex gap-3 rounded-[12px] border border-[#dfeaf6] bg-white p-3.5 transition-colors hover:bg-[#F5F7FA] dark:border-[#2a2a2d] dark:bg-[#1f1f22] dark:hover:bg-[#2a2a2d]"
>
{(onRemoveTask || onRetryTask) ? (
<div className="absolute right-2 top-2 flex items-center gap-2">
{onRetryTask && (item.status === 'failed' || item.status === 'partial_failed') ? (
<button
type="button"
className="text-[11px] text-[#2B7FFF] transition-colors hover:text-[#1d4ed8]"
onClick={() => onRetryTask(item.id)}
>
</button>
) : null}
{onRemoveTask ? (
<button
type="button"
className="text-[11px] text-[#99A0AE] transition-colors hover:text-[#ef4444]"
onClick={() => onRemoveTask(item.removeTaskId || item.id)}
>
</button>
) : null}
</div>
) : null}
<div className="flex h-11 w-11 items-center justify-center rounded-lg border border-dashed border-[#9fc0e8] bg-[#EFF6FF] text-[23px] text-[#3b82f6] dark:border-gray-700 dark:bg-[#222225]">
</div>
<div className="min-w-0 flex-1">
<div className="text-sm font-semibold text-[#171717] dark:text-gray-100">{item.title}</div>
<div className="mt-1.5 text-[13px] leading-6 text-[#9aa5b1] dark:text-gray-400">{item.description}</div>
{item.meta ? (
<div className="mt-1 text-[11px] text-[#99A0AE] dark:text-gray-500">{item.meta}</div>
) : null}
<div className="mt-2 text-xs font-medium text-[#2B7FFF]">{item.status}</div>
</div>
</article>
))
) : (
<div className="rounded-[12px] border border-dashed border-[#dfeaf6] bg-[#f8fbff] px-4 py-6 text-sm text-[#99A0AE] dark:border-[#2a2a2d] dark:bg-[#1f1f22] dark:text-gray-400">
</div>
)}
</div>
</div>
</aside>
);
}

View File

@@ -0,0 +1,5 @@
export { default as ChatComposer } from './ChatComposer';
export { default as ChatHistoryPanel } from './ChatHistoryPanel';
export { default as ChatMessageList } from './ChatMessageList';
export { default as TaskBoard } from './TaskBoard';
export type { ChatHistoryBucket, ChatMessageItem, TaskItem, TaskTabValue } from './types';

View File

@@ -0,0 +1,33 @@
import type { AttachedFileMeta } from '../../shared/chat-model';
export type TaskTabValue = 'pending' | 'completed';
export type ChatHistoryBucket = {
key: string;
label: string;
sessions: Array<{
conversationId: string;
title: string;
updatedAt: string;
}>;
};
export type ChatMessageItem = {
id: string;
role: 'assistant' | 'user';
name: string;
time: string;
content: string;
attachments?: AttachedFileMeta[];
isStreaming?: boolean;
isError?: boolean;
};
export type TaskItem = {
id: string;
removeTaskId?: string;
title: string;
description: string;
status: string;
meta?: string;
};

View File

@@ -0,0 +1,23 @@
import { Outlet } from 'react-router-dom';
import Sidebar from './Sidebar';
import TitleBar from './TitleBar';
export default function MainLayout() {
const platform = (window as any).api?.platform ?? '';
return (
<div className="bg-white dark:!bg-[#0f0f10] h-screen flex flex-col">
<TitleBar />
<main
className="box-border w-full flex pt-2 pb-2 pl-2"
style={{ height: platform === 'linux' ? '100vh' : 'calc(100vh - 40px)' }}
>
<div className="flex-1 flex min-w-0 min-h-0">
<Outlet />
</div>
<Sidebar />
</main>
</div>
);
}

View File

@@ -0,0 +1,67 @@
import { useLocation, useNavigate } from 'react-router-dom';
import { NAV_ITEMS, normalizeWorkspacePath } from '../../router/routes';
const MENU_MARKS: Record<string, string> = {
'/home': '首',
'/knowledge': '知',
'/agents': '模',
'/skills': '技',
'/cron': '时',
'/scripts': '脚',
'/setting': '设',
};
export default function Sidebar() {
const location = useLocation();
const navigate = useNavigate();
const currentId = normalizeWorkspacePath(location.pathname);
return (
<aside className="w-[80px] h-full box-border flex flex-col items-center pb-[8px]">
<div className="flex flex-col gap-[16px] w-full">
{NAV_ITEMS.map((item) => {
const active = currentId === item.path;
const isSetting = item.path === '/setting';
return (
<div
key={item.path}
className={['flex flex-col gap-[16px]', isSetting ? 'mt-auto mb-[8px] shrink-1' : ''].join(' ')}
>
<button
type="button"
className="cursor-pointer flex flex-col items-center justify-center"
onClick={() => navigate(item.path)}
>
<div
className={[
'box-border rounded-[16px] w-[48px] h-[48px] flex flex-col items-center justify-center hover:bg-white dark:hover:bg-[#222225]',
active ? 'bg-white dark:bg-[#222225]' : '',
].join(' ')}
>
<span
className="text-[18px] font-semibold leading-none"
style={{ color: active ? '#2B7FFF' : '#525866' }}
>
{MENU_MARKS[item.path]}
</span>
</div>
<div
className="text-[14px] mt-[4px] mb-[8px] hover:text-[#2B7FFF]"
style={{ color: active ? '#2B7FFF' : '#525866' }}
>
{item.label}
</div>
</button>
</div>
);
})}
</div>
<div className="w-[48px] h-[48px] rounded-full overflow-hidden mt-auto bg-white dark:bg-[#222225] flex items-center justify-center border border-black/10 dark:border-[#2a2a2d]">
<span className="text-[16px] font-bold text-[#2B7FFF]">Z</span>
</div>
</aside>
);
}

View File

@@ -0,0 +1,71 @@
type TitleBarProps = {
variant?: 'default' | 'light';
};
export default function TitleBar({ variant = 'default' }: TitleBarProps) {
const platform = (window as any).api?.platform ?? '';
if (platform === 'linux') return null;
const iconColorClass =
variant === 'light' ? 'text-white' : 'text-[#525866] dark:text-gray-300';
const borderColorClass =
variant === 'light' ? 'border-b-white/30' : 'border-b-gray-300 dark:border-gray-700';
if (platform === 'darwin') {
return (
<div
className={['drag-region h-10 shrink-0 border-b', borderColorClass].join(' ')}
style={{ background: 'transparent' }}
/>
);
}
return (
<div
className={['drag-region flex h-10 shrink-0 items-center justify-end border-b', borderColorClass].join(
' ',
)}
style={{ background: 'transparent' }}
>
<div className="no-drag flex h-full">
<button
type="button"
className={['flex h-full w-11 items-center justify-center hover:bg-[#999] hover:text-white transition-colors', iconColorClass].join(
' ',
)}
title="Minimize"
onClick={() => {
(window as any).api?.windowMinimize?.();
}}
>
<span className="text-base leading-none"></span>
</button>
<button
type="button"
className={['flex h-full w-11 items-center justify-center hover:bg-[#999] hover:text-white transition-colors', iconColorClass].join(
' ',
)}
title="Maximize"
onClick={() => {
(window as any).api?.windowMaximize?.();
}}
>
<span className="text-[14px] leading-none"></span>
</button>
<button
type="button"
className={['flex h-full w-11 items-center justify-center hover:bg-[#ff0000] hover:text-white transition-colors', iconColorClass].join(
' ',
)}
title="Close"
onClick={() => {
(window as any).api?.windowClose?.();
}}
>
<span className="text-base leading-none">×</span>
</button>
</div>
</div>
);
}

View File

@@ -0,0 +1,3 @@
export { default as MainLayout } from './MainLayout';
export { default as Sidebar } from './Sidebar';
export { default as TitleBar } from './TitleBar';