Merge pull request 'feat: 一些优化' (#7) from feat/0.4.0 into main
my-v0 / deploy (push) Successful in 10s
my-v0 / deploy (push) Successful in 10s
Reviewed-on: #7
This commit was merged in pull request #7.
This commit is contained in:
@@ -40,3 +40,4 @@ __screenshots__/
|
||||
|
||||
AGENTS.md
|
||||
.agents
|
||||
skills-lock.json
|
||||
@@ -95,6 +95,7 @@ const onCommand = (command: keyof typeof commendMap) => {
|
||||
:scale="scale"
|
||||
:lines="lines"
|
||||
@zoomchange="onZoomChange"
|
||||
@mousedown="onClearSelect"
|
||||
>
|
||||
<div
|
||||
ref="stageRef"
|
||||
@@ -107,6 +108,7 @@ const onCommand = (command: keyof typeof commendMap) => {
|
||||
@dragover.prevent
|
||||
@drop="onDrop"
|
||||
@mousedown.self="onClearSelect"
|
||||
@mousedown.stop
|
||||
>
|
||||
<el-dropdown
|
||||
v-for="(node, index) in nodes"
|
||||
|
||||
@@ -72,6 +72,13 @@ export const useAgentChat = () => {
|
||||
if (!assistant.content) assistant.content = text
|
||||
}
|
||||
|
||||
/** 中断后收口:有内容就定稿,空气泡直接丢掉 */
|
||||
const settleAborted = (assistant: AgentChatMessage) => {
|
||||
assistant.final = true
|
||||
if (assistant.content || assistant.patches.length) return
|
||||
messages.value = messages.value.filter((item) => item.id !== assistant.id)
|
||||
}
|
||||
|
||||
/** token 拼进 content;有 progress 才记;stop 帧根上才 setPage / 收 patches */
|
||||
const handleFrame = (assistant: AgentChatMessage, frame: StreamFrame) => {
|
||||
const choice = frame.choices?.[0]
|
||||
@@ -153,7 +160,7 @@ export const useAgentChat = () => {
|
||||
}),
|
||||
})
|
||||
if (!res.ok) {
|
||||
let detail = `指令失败 (${res.status})`
|
||||
let detail = `错误 (${res.status} ${res.statusText})`
|
||||
try {
|
||||
const body = await res.json()
|
||||
if (body.errors?.length) detail = body.errors.join(';')
|
||||
@@ -164,9 +171,13 @@ export const useAgentChat = () => {
|
||||
}
|
||||
if (!res.body) throw new Error('没有响应体')
|
||||
await consumeStream(res.body.getReader(), live)
|
||||
if (!live.final) failAssistant(live, '没有收到结果')
|
||||
if (abortController?.signal.aborted) settleAborted(live)
|
||||
else if (!live.final) failAssistant(live, '没有收到结果')
|
||||
} catch (err) {
|
||||
if (isAbortError(err)) return
|
||||
if (isAbortError(err)) {
|
||||
settleAborted(live)
|
||||
return
|
||||
}
|
||||
const text = err instanceof Error ? err.message : '指令失败'
|
||||
failAssistant(live, text)
|
||||
ElMessage.error(text)
|
||||
@@ -175,6 +186,12 @@ export const useAgentChat = () => {
|
||||
}
|
||||
}
|
||||
|
||||
/** 用户点停止:中断 fetch,已输出的 token 保留 */
|
||||
const stop = () => {
|
||||
if (!running.value) return
|
||||
abortController?.abort()
|
||||
}
|
||||
|
||||
const clear = () => {
|
||||
abortController?.abort()
|
||||
abortController = null
|
||||
@@ -186,5 +203,5 @@ export const useAgentChat = () => {
|
||||
abortController?.abort()
|
||||
})
|
||||
|
||||
return { messages, running, send, clear }
|
||||
return { messages, running, send, stop, clear }
|
||||
}
|
||||
|
||||
@@ -12,12 +12,13 @@ defineOptions({
|
||||
|
||||
const editorStore = useEditorStore()
|
||||
const { selectedNodeIds } = storeToRefs(editorStore)
|
||||
const { messages, running, send, clear } = useAgentChat()
|
||||
const { messages, running, send, stop, clear } = useAgentChat()
|
||||
|
||||
// 输入
|
||||
const draft = ref('')
|
||||
const scrollRootRef = useTemplateRef<HTMLElement>('scrollRootRef')
|
||||
const contentRootRef = useTemplateRef<HTMLElement>('contentRootRef')
|
||||
const livePatchListRef = useTemplateRef<HTMLElement>('livePatchListRef')
|
||||
const { scheduleScrollToBottom } = useStickToBottom(scrollRootRef, contentRootRef)
|
||||
|
||||
const selectedHint = computed(() => {
|
||||
@@ -27,20 +28,43 @@ const selectedHint = computed(() => {
|
||||
|
||||
const canSend = computed(() => Boolean(draft.value.trim()) && !running.value)
|
||||
|
||||
/** 正在输出的那条 bot(对话末尾) */
|
||||
const lastAssistantId = computed(() => {
|
||||
for (let i = messages.value.length - 1; i >= 0; i--) {
|
||||
if (messages.value[i]?.role === 'assistant') return messages.value[i]?.id
|
||||
}
|
||||
return ''
|
||||
})
|
||||
|
||||
/** 只盯最后一条,避免每个 token 深监听整份对话 */
|
||||
const latestOutputSignal = computed(() => {
|
||||
const latest = messages.value[messages.value.length - 1]
|
||||
if (!latest) return '0'
|
||||
return `${messages.value.length}:${latest.id}:${latest.content.length}:${latest.final}:${latest.patches.length}`
|
||||
})
|
||||
|
||||
/** 当前回复的补丁列表超出时跟到最底 */
|
||||
const stickPatchList = () => {
|
||||
const el = livePatchListRef.value
|
||||
if (!el || el.scrollHeight <= el.clientHeight) return
|
||||
el.scrollTop = el.scrollHeight
|
||||
}
|
||||
|
||||
watch(
|
||||
messages,
|
||||
latestOutputSignal,
|
||||
async () => {
|
||||
await nextTick()
|
||||
scheduleScrollToBottom()
|
||||
stickPatchList()
|
||||
},
|
||||
{ deep: true },
|
||||
{ flush: 'post' },
|
||||
)
|
||||
|
||||
/** Enter 发送,Shift+Enter 换行 */
|
||||
/** Enter 发送,Shift+Enter 换行;输出中 Enter 不打断 */
|
||||
const onKeydown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
onSend()
|
||||
if (!running.value) onSend()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +74,12 @@ const onSend = () => {
|
||||
draft.value = ''
|
||||
send(text)
|
||||
}
|
||||
|
||||
/** 输出中点停止,否则发送 */
|
||||
const onAction = () => {
|
||||
if (running.value) stop()
|
||||
else onSend()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -66,18 +96,18 @@ const onSend = () => {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div ref="scrollRootRef" class="flex-1 min-h-0">
|
||||
<div ref="scrollRootRef" class="flex-1 min-h-0 overflow-auto overscroll-contain" tabindex="0">
|
||||
<div
|
||||
ref="contentRootRef"
|
||||
class="h-full flex flex-col gap-3 p-3 overflow-auto"
|
||||
:class="{ 'items-center justify-center': !messages.length }"
|
||||
class="flex min-h-full flex-col gap-3 p-3"
|
||||
:class="{ 'h-full items-center justify-center': !messages.length }"
|
||||
>
|
||||
<div
|
||||
v-if="!messages.length"
|
||||
class="flex flex-col items-center justify-center gap-2 py-10 text-sm text-white/50"
|
||||
>
|
||||
<Icon icon="solar:chat-round-dots-bold" width="28" />
|
||||
<p>用自然语言改画布</p>
|
||||
<p>用自然语言修改画布</p>
|
||||
<p class="text-xs text-white/35">试试:加一张柱状图</p>
|
||||
</div>
|
||||
|
||||
@@ -92,9 +122,12 @@ const onSend = () => {
|
||||
<div v-else class="flex justify-start">
|
||||
<div
|
||||
class="max-w-[90%] min-w-0 rounded-lg bg-white/5 px-2.5 py-2 text-sm text-(--el-text-color-regular)"
|
||||
:class="{
|
||||
glow: running && msg.id === lastAssistantId,
|
||||
}"
|
||||
>
|
||||
<Icon
|
||||
v-if="!msg.content && !msg.patches.length"
|
||||
v-if="!msg.final && !msg.content && !msg.patches.length"
|
||||
icon="svg-spinners:3-dots-fade"
|
||||
width="20"
|
||||
class="text-white/50"
|
||||
@@ -112,7 +145,8 @@ const onSend = () => {
|
||||
</div>
|
||||
<div
|
||||
v-if="msg.patches.length"
|
||||
class="mt-2 flex flex-col gap-1 font-mono text-xs text-white/45"
|
||||
:ref="msg.id === lastAssistantId ? 'livePatchListRef' : undefined"
|
||||
class="mt-2 flex flex-col gap-1 font-mono text-xs text-white/45 max-h-26 overflow-y-auto"
|
||||
>
|
||||
<div v-for="(patch, index) in msg.patches" :key="index">
|
||||
{{ patch.op }}
|
||||
@@ -131,7 +165,7 @@ const onSend = () => {
|
||||
v-model="draft"
|
||||
rows="3"
|
||||
class="w-full resize-none bg-transparent text-sm text-(--el-text-color-regular) outline-none placeholder:text-white/35"
|
||||
placeholder="描述你想改的画布,Enter 发送"
|
||||
placeholder="描述你想修改的画布,Enter 发送"
|
||||
:disabled="running"
|
||||
@keydown="onKeydown"
|
||||
/>
|
||||
@@ -139,11 +173,14 @@ const onSend = () => {
|
||||
<span class="text-xs text-white/35">{{ selectedHint }}</span>
|
||||
<span
|
||||
class="border p-1 rounded-lg cursor-pointer"
|
||||
:class="{ 'opacity-50 cursor-not-allowed': !canSend }"
|
||||
title="发送"
|
||||
@click="onSend"
|
||||
:class="{ 'opacity-50 cursor-not-allowed': !canSend && !running }"
|
||||
:title="running ? '停止' : '发送'"
|
||||
@click="onAction"
|
||||
>
|
||||
<Icon icon="solar:plain-2-bold" width="14" />
|
||||
<span v-if="running" class="flex h-[14px] w-[14px] items-center justify-center">
|
||||
<span class="block h-2.5 w-2.5 rounded-[2px] bg-current" />
|
||||
</span>
|
||||
<Icon v-else icon="solar:plain-2-bold" width="14" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -155,4 +192,47 @@ const onSend = () => {
|
||||
:deep(.markdownRender .paragraph-node) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@property --a {
|
||||
syntax: '<angle>';
|
||||
inherits: false;
|
||||
initial-value: 0deg;
|
||||
}
|
||||
|
||||
.glow {
|
||||
--a: 0deg;
|
||||
position: relative;
|
||||
background: color-mix(in oklab, var(--color-white) 5%, transparent);
|
||||
}
|
||||
|
||||
.glow::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: inherit;
|
||||
padding: 2px; /* 边框厚度 */
|
||||
pointer-events: none;
|
||||
background: conic-gradient(
|
||||
from var(--a),
|
||||
transparent 0%,
|
||||
transparent 58%,
|
||||
#6b7280 66%,
|
||||
#d7dde4 73%,
|
||||
#ffffff 78%,
|
||||
#b8c0c8 84%,
|
||||
transparent 92%
|
||||
);
|
||||
animation: spin-a 2.4s linear infinite;
|
||||
-webkit-mask:
|
||||
linear-gradient(#fff 0 0) content-box,
|
||||
linear-gradient(#fff 0 0);
|
||||
-webkit-mask-composite: xor;
|
||||
mask-composite: exclude;
|
||||
}
|
||||
|
||||
@keyframes spin-a {
|
||||
to {
|
||||
--a: 360deg;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -87,7 +87,7 @@ const onConfirmEvent = () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="px-2">
|
||||
<div class="h-full px-2">
|
||||
<div class="flex items-center justify-between font-bold p-1">
|
||||
<span>{{ selectedNode?.name }}</span>
|
||||
<div class="flex gap-2">
|
||||
@@ -100,41 +100,43 @@ const onConfirmEvent = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-tabs v-model="activeTab" type="border-card" stretch>
|
||||
<el-tab-pane label="属性" name="property">
|
||||
<el-collapse v-model="activeNames">
|
||||
<el-collapse-item title="布局属性" name="layout">
|
||||
<FormCreate :setters="layoutSetters" :form-data="selectedNode" />
|
||||
</el-collapse-item>
|
||||
<el-collapse-item title="组件属性" name="component">
|
||||
<FormCreate :setters="setters" :form-data="selectedNode" />
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</el-tab-pane>
|
||||
<div class="h-full overflow-y-auto">
|
||||
<el-tabs v-model="activeTab" type="border-card" stretch>
|
||||
<el-tab-pane label="属性" name="property">
|
||||
<el-collapse v-model="activeNames">
|
||||
<el-collapse-item title="布局属性" name="layout">
|
||||
<FormCreate :setters="layoutSetters" :form-data="selectedNode" />
|
||||
</el-collapse-item>
|
||||
<el-collapse-item title="组件属性" name="component">
|
||||
<FormCreate :setters="setters" :form-data="selectedNode" />
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="数据源" name="data-source">
|
||||
<DataSource />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<el-tab-pane label="数据源" name="data-source">
|
||||
<DataSource />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<!-- 编辑 JSON -->
|
||||
<el-drawer v-model="jsonVisible" title="编辑 JSON" class="json-drawer" destroy-on-close>
|
||||
<MonacoEditor v-model="jsonContent" lang="json" />
|
||||
<!-- 编辑 JSON -->
|
||||
<el-drawer v-model="jsonVisible" title="编辑 JSON" class="json-drawer" destroy-on-close>
|
||||
<MonacoEditor v-model="jsonContent" lang="json" />
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="jsonVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="onConfirm">确认</el-button>
|
||||
</template>
|
||||
</el-drawer>
|
||||
<template #footer>
|
||||
<el-button @click="jsonVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="onConfirm">确认</el-button>
|
||||
</template>
|
||||
</el-drawer>
|
||||
|
||||
<!-- 事件配置 -->
|
||||
<el-dialog destroy-on-close title="事件配置" width="800" v-model="eventVisible">
|
||||
<NodeEvents ref="nodeEvents" />
|
||||
<template #footer>
|
||||
<el-button @click="eventVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="onConfirmEvent">确认</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<!-- 事件配置 -->
|
||||
<el-dialog destroy-on-close title="事件配置" width="800" v-model="eventVisible">
|
||||
<NodeEvents ref="nodeEvents" />
|
||||
<template #footer>
|
||||
<el-button @click="eventVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="onConfirmEvent">确认</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -9,17 +9,27 @@ const props = defineProps<{
|
||||
schema: MaterialSchema
|
||||
}>()
|
||||
|
||||
const justifyMap: Record<string, string> = {
|
||||
left: 'flex-start',
|
||||
center: 'center',
|
||||
right: 'flex-end',
|
||||
}
|
||||
|
||||
const textStyle = computed(() => {
|
||||
const style = props.schema.style
|
||||
const textAlign = style?.textAlign ?? 'center'
|
||||
return {
|
||||
...style,
|
||||
fontSize: style?.fontSize ? style.fontSize + 'px' : undefined,
|
||||
textAlign,
|
||||
justifyContent: justifyMap[textAlign] ?? 'center',
|
||||
alignItems: style?.alignItems ?? 'center',
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :style="textStyle">{{ schema.props.content }}</div>
|
||||
<div :style="textStyle" class="flex h-full w-full min-w-0">{{ schema.props.content }}</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
@@ -22,6 +22,30 @@ const textMaterial: MaterialDefinition = {
|
||||
label: '字号',
|
||||
key: 'style.fontSize',
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
label: '水平对齐',
|
||||
key: 'style.textAlign',
|
||||
props: {
|
||||
options: [
|
||||
{ label: '左对齐', value: 'left' },
|
||||
{ label: '居中', value: 'center' },
|
||||
{ label: '右对齐', value: 'right' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
label: '垂直对齐',
|
||||
key: 'style.alignItems',
|
||||
props: {
|
||||
options: [
|
||||
{ label: '顶部', value: 'flex-start' },
|
||||
{ label: '居中', value: 'center' },
|
||||
{ label: '底部', value: 'flex-end' },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
eventOptions: commonEventOptions,
|
||||
schema: {
|
||||
@@ -36,6 +60,8 @@ const textMaterial: MaterialDefinition = {
|
||||
style: {
|
||||
color: '#fff',
|
||||
fontSize: 16,
|
||||
textAlign: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
props: {
|
||||
content: '文本内容',
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
@import 'tailwindcss';
|
||||
@import 'markstream-vue/index.css' layer(components);
|
||||
|
||||
html {
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
@supports not selector(::-webkit-scrollbar) {
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
|
||||
Reference in New Issue
Block a user