2026-03-02 16:49:55 +08:00
|
|
|
import { customElement, noShadowDOM } from 'solid-element';
|
2026-03-03 10:17:39 +08:00
|
|
|
import { For, Show, createEffect } from 'solid-js';
|
|
|
|
|
import type {TextResult, RuntimeResult, OptionsResult} from '../yarn-spinner/runtime/results';
|
|
|
|
|
import { createYarnStore } from './stores/yarnStore';
|
2026-03-02 16:49:55 +08:00
|
|
|
|
2026-03-13 15:36:35 +08:00
|
|
|
export interface YarnSpinnerProps {
|
|
|
|
|
start: string;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-03 14:57:46 +08:00
|
|
|
customElement<{start: string}>('md-yarn-spinner', {start: 'start'}, (props, { element }) => {
|
2026-03-02 16:49:55 +08:00
|
|
|
noShadowDOM();
|
|
|
|
|
|
2026-03-03 10:17:39 +08:00
|
|
|
let historyContainer: HTMLDivElement | undefined;
|
|
|
|
|
|
|
|
|
|
const { store, advance, restart } = createYarnStore(element as any, props);
|
|
|
|
|
|
|
|
|
|
// 滚动到底部
|
|
|
|
|
const scrollToBottom = () => {
|
|
|
|
|
if (historyContainer) {
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
historyContainer!.scrollTop = historyContainer!.scrollHeight;
|
|
|
|
|
}, 0);
|
2026-03-02 16:49:55 +08:00
|
|
|
}
|
|
|
|
|
};
|
2026-03-03 10:17:39 +08:00
|
|
|
|
|
|
|
|
// 监听对话历史变化,自动滚动到底部
|
2026-03-02 16:49:55 +08:00
|
|
|
createEffect(() => {
|
2026-03-03 10:17:39 +08:00
|
|
|
store.dialogueHistory;
|
|
|
|
|
scrollToBottom();
|
2026-03-02 16:49:55 +08:00
|
|
|
});
|
2026-03-03 10:17:39 +08:00
|
|
|
|
2026-03-02 16:49:55 +08:00
|
|
|
// 渲染对话历史项
|
2026-03-03 10:17:39 +08:00
|
|
|
const renderEntry = (result: RuntimeResult | OptionsResult['options'][0]) => {
|
|
|
|
|
if(!('type' in result)){
|
|
|
|
|
return <div class="dialogue-entry text-entry">
|
|
|
|
|
<span class="text"> {"->"} {result.text}</span>
|
|
|
|
|
</div>
|
|
|
|
|
}
|
2026-03-02 16:49:55 +08:00
|
|
|
if (result.type === 'text') {
|
|
|
|
|
const textResult = result as TextResult;
|
|
|
|
|
return (
|
|
|
|
|
<div class="dialogue-entry text-entry">
|
|
|
|
|
<Show when={textResult.speaker}>
|
|
|
|
|
<span class="speaker font-bold text-blue-600">{textResult.speaker}: </span>
|
|
|
|
|
</Show>
|
|
|
|
|
<span class="text">{textResult.text}</span>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-03-03 10:17:39 +08:00
|
|
|
|
2026-03-02 16:49:55 +08:00
|
|
|
if (result.type === 'command') {
|
|
|
|
|
return (
|
|
|
|
|
<div class="dialogue-entry command-entry text-gray-500 italic">
|
|
|
|
|
[{result.command}]
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-03-03 10:17:39 +08:00
|
|
|
|
2026-03-02 16:49:55 +08:00
|
|
|
return null;
|
|
|
|
|
};
|
2026-03-03 10:17:39 +08:00
|
|
|
|
2026-03-02 16:49:55 +08:00
|
|
|
return (
|
2026-03-03 10:17:39 +08:00
|
|
|
<div class="w-full max-w-2xl mx-auto shadow-sm relative">
|
2026-03-02 16:49:55 +08:00
|
|
|
{/* 对话历史 */}
|
2026-03-03 10:17:39 +08:00
|
|
|
<div
|
|
|
|
|
ref={historyContainer}
|
|
|
|
|
class="dialogue-history p-4 h-64 overflow-y-auto bg-gray-50"
|
|
|
|
|
>
|
|
|
|
|
<Show when={store.dialogueHistory.length === 0 && !store.runnerInstance}>
|
2026-03-02 16:49:55 +08:00
|
|
|
<div class="text-gray-400 text-center py-8">点击重新开始开始对话</div>
|
|
|
|
|
</Show>
|
2026-03-03 10:17:39 +08:00
|
|
|
<For each={store.dialogueHistory}>
|
2026-03-02 17:54:40 +08:00
|
|
|
{renderEntry}
|
2026-03-02 16:49:55 +08:00
|
|
|
</For>
|
|
|
|
|
</div>
|
2026-03-03 00:48:13 +08:00
|
|
|
|
2026-03-03 10:17:39 +08:00
|
|
|
{/* 浮动工具栏 */}
|
|
|
|
|
<div class="toolbar absolute right-0 rounded-bl-lg shadow-sm flex">
|
|
|
|
|
<button
|
|
|
|
|
onClick={restart}
|
|
|
|
|
class="restart-button px-3 py-1 text-sm bg-gray-200 hover:bg-gray-300
|
|
|
|
|
rounded-bl-md transition-colors cursor-pointer"
|
|
|
|
|
title="重新开始"
|
|
|
|
|
>
|
|
|
|
|
🔄
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => advance()}
|
|
|
|
|
class="advance-button px-3 py-1 text-sm bg-gray-200 hover:bg-gray-300
|
|
|
|
|
transition-colors cursor-pointer"
|
|
|
|
|
title="继续"
|
|
|
|
|
>
|
|
|
|
|
⏩
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-03-02 16:49:55 +08:00
|
|
|
{/* 当前选项 */}
|
2026-03-03 10:17:39 +08:00
|
|
|
<div class="current-options p-4 border-t border-gray-300 bg-white min-h-34">
|
2026-03-02 16:49:55 +08:00
|
|
|
<div class="options-list flex flex-col gap-2">
|
2026-03-03 10:17:39 +08:00
|
|
|
<For each={store.currentOptions?.options || []}>
|
2026-03-02 16:49:55 +08:00
|
|
|
{(option, index) => (
|
|
|
|
|
<button
|
2026-03-02 17:54:40 +08:00
|
|
|
onClick={() => advance(index())}
|
2026-03-03 00:48:13 +08:00
|
|
|
class="option-button text-left px-4 py-2 bg-blue-50 hover:bg-blue-100
|
2026-03-02 16:49:55 +08:00
|
|
|
rounded border border-blue-200 transition-colors cursor-pointer"
|
|
|
|
|
>
|
|
|
|
|
→ {option.text}
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
</For>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
});
|