refactor: reorg
This commit is contained in:
parent
6b77653d27
commit
4280da9fec
|
|
@ -1,105 +1,38 @@
|
||||||
import { customElement, noShadowDOM } from 'solid-element';
|
import { customElement, noShadowDOM } from 'solid-element';
|
||||||
import {createSignal, createResource, For, Show, createEffect} from 'solid-js';
|
import { For, Show, createEffect } from 'solid-js';
|
||||||
import { parseYarn, compile, YarnRunner } from '../yarn-spinner';
|
import type {TextResult, RuntimeResult, OptionsResult} from '../yarn-spinner/runtime/results';
|
||||||
import type { RunnerOptions } from '../yarn-spinner/runtime/runner';
|
import { createYarnStore } from './stores/yarnStore';
|
||||||
import type { RuntimeResult, TextResult, OptionsResult } from '../yarn-spinner/runtime/results';
|
import {RunnerOptions} from "../yarn-spinner/runtime/runner";
|
||||||
import {loadElementSrc, resolvePath} from "./utils/path";
|
|
||||||
|
|
||||||
// 缓存已加载的 yarn 文件内容
|
customElement<RunnerOptions>('md-yarn-spinner', {startAt: 'start'}, (props, { element }) => {
|
||||||
const yarnCache = new Map<string, string>();
|
|
||||||
|
|
||||||
// 加载 yarn 文件内容
|
|
||||||
async function loadYarnFile(path: string): Promise<string> {
|
|
||||||
if (yarnCache.has(path)) {
|
|
||||||
return yarnCache.get(path)!;
|
|
||||||
}
|
|
||||||
const response = await fetch(path);
|
|
||||||
const content = await response.text();
|
|
||||||
yarnCache.set(path, content);
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载多个 yarn 文件并拼接
|
|
||||||
async function loadYarnFiles(paths: string[]): Promise<string> {
|
|
||||||
const contents = await Promise.all(paths.map(path => loadYarnFile(path)));
|
|
||||||
return contents.join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
customElement<RunnerOptions>('md-yarn-spinner', {
|
|
||||||
startAt: "start",
|
|
||||||
}, (props, { element }) => {
|
|
||||||
noShadowDOM();
|
noShadowDOM();
|
||||||
|
|
||||||
const [dialogueHistory, setDialogueHistory] = createSignal<RuntimeResult[]>([]);
|
let historyContainer: HTMLDivElement | undefined;
|
||||||
const [currentOptions, setCurrentOptions] = createSignal<OptionsResult | null>(null);
|
|
||||||
const [isEnded, setIsEnded] = createSignal(false);
|
const { store, advance, restart } = createYarnStore(element as any, props);
|
||||||
const [runnerInstance, setRunnerInstance] = createSignal<YarnRunner | null>(null);
|
|
||||||
|
// 滚动到底部
|
||||||
// 获取文件路径
|
const scrollToBottom = () => {
|
||||||
const {articlePath, rawSrc} = loadElementSrc(element);
|
if (historyContainer) {
|
||||||
const yarnPaths = (rawSrc || '').split(',')
|
setTimeout(() => {
|
||||||
.map((s: string) => resolvePath(articlePath, s.trim()))
|
historyContainer!.scrollTop = historyContainer!.scrollHeight;
|
||||||
.filter(Boolean);
|
}, 0);
|
||||||
|
|
||||||
// 加载 yarn 内容
|
|
||||||
const [yarnContent] = createResource(() => yarnPaths, loadYarnFiles);
|
|
||||||
|
|
||||||
// 创建 runner
|
|
||||||
const createRunner = () => {
|
|
||||||
const content = yarnContent();
|
|
||||||
if (!content) return null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const ast = parseYarn(content);
|
|
||||||
const program = compile(ast);
|
|
||||||
|
|
||||||
return new YarnRunner(program, props);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to initialize YarnRunner:', error);
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function advance(index?: number){
|
// 监听对话历史变化,自动滚动到底部
|
||||||
const runner = runnerInstance();
|
|
||||||
if(!runner) return;
|
|
||||||
if(runner.currentResult?.type !== 'options' && runner.currentResult?.isDialogueEnd) return;
|
|
||||||
runner.advance(index);
|
|
||||||
processRunnerOutput(runner);
|
|
||||||
}
|
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if(!yarnContent()) return;
|
store.dialogueHistory;
|
||||||
setRunnerInstance(createRunner());
|
scrollToBottom();
|
||||||
processRunnerOutput(runnerInstance()!);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 处理 runner 输出
|
|
||||||
const processRunnerOutput = (runner: YarnRunner) => {
|
|
||||||
const result = runner.currentResult;
|
|
||||||
if (!result) return;
|
|
||||||
|
|
||||||
if(result.type === 'options'){
|
|
||||||
setCurrentOptions(result);
|
|
||||||
}else{
|
|
||||||
setCurrentOptions(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
setDialogueHistory([...runner.history]);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 重新开始
|
|
||||||
const restart = () => {
|
|
||||||
setDialogueHistory([]);
|
|
||||||
setCurrentOptions(null);
|
|
||||||
setIsEnded(false);
|
|
||||||
setRunnerInstance(createRunner());
|
|
||||||
processRunnerOutput(runnerInstance()!);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 渲染对话历史项
|
// 渲染对话历史项
|
||||||
const renderEntry = (result: RuntimeResult) => {
|
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>
|
||||||
|
}
|
||||||
if (result.type === 'text') {
|
if (result.type === 'text') {
|
||||||
const textResult = result as TextResult;
|
const textResult = result as TextResult;
|
||||||
return (
|
return (
|
||||||
|
|
@ -111,7 +44,7 @@ customElement<RunnerOptions>('md-yarn-spinner', {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.type === 'command') {
|
if (result.type === 'command') {
|
||||||
return (
|
return (
|
||||||
<div class="dialogue-entry command-entry text-gray-500 italic">
|
<div class="dialogue-entry command-entry text-gray-500 italic">
|
||||||
|
|
@ -119,30 +52,49 @@ customElement<RunnerOptions>('md-yarn-spinner', {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="yarn-spinner w-full max-w-2xl mx-auto shadow-sm relative">
|
<div class="w-full max-w-2xl mx-auto shadow-sm relative">
|
||||||
{/* 对话历史 */}
|
{/* 对话历史 */}
|
||||||
<div class="dialogue-history p-4 h-64 overflow-y-auto bg-gray-50">
|
<div
|
||||||
<Show when={dialogueHistory().length === 0 && !yarnContent.loading}>
|
ref={historyContainer}
|
||||||
|
class="dialogue-history p-4 h-64 overflow-y-auto bg-gray-50"
|
||||||
|
>
|
||||||
|
<Show when={store.dialogueHistory.length === 0 && !store.runnerInstance}>
|
||||||
<div class="text-gray-400 text-center py-8">点击重新开始开始对话</div>
|
<div class="text-gray-400 text-center py-8">点击重新开始开始对话</div>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={yarnContent.loading}>
|
<For each={store.dialogueHistory}>
|
||||||
<div class="text-gray-400 text-center py-8">加载对话中...</div>
|
|
||||||
</Show>
|
|
||||||
<For each={dialogueHistory()}>
|
|
||||||
{renderEntry}
|
{renderEntry}
|
||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 浮动工具栏 */}
|
||||||
|
<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>
|
||||||
|
|
||||||
{/* 当前选项 */}
|
{/* 当前选项 */}
|
||||||
<Show when={currentOptions() && !isEnded()}>
|
<div class="current-options p-4 border-t border-gray-300 bg-white min-h-34">
|
||||||
<div class="current-options p-4 border-t bg-white">
|
|
||||||
<div class="options-list flex flex-col gap-2">
|
<div class="options-list flex flex-col gap-2">
|
||||||
<For each={currentOptions()?.options || []}>
|
<For each={store.currentOptions?.options || []}>
|
||||||
{(option, index) => (
|
{(option, index) => (
|
||||||
<button
|
<button
|
||||||
onClick={() => advance(index())}
|
onClick={() => advance(index())}
|
||||||
|
|
@ -155,27 +107,6 @@ customElement<RunnerOptions>('md-yarn-spinner', {
|
||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
|
||||||
|
|
||||||
{/* 浮动工具栏 */}
|
|
||||||
<div class="toolbar absolute top-0 right-0 p-2 bg-gray-100 border-t border-l rounded-tl-lg shadow-sm flex gap-2">
|
|
||||||
<button
|
|
||||||
onClick={restart}
|
|
||||||
class="restart-button px-3 py-1 text-sm bg-gray-200 hover:bg-gray-300
|
|
||||||
rounded 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
|
|
||||||
rounded transition-colors cursor-pointer"
|
|
||||||
title="继续"
|
|
||||||
>
|
|
||||||
⏩ 继续
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,122 @@
|
||||||
|
import {createEffect, createResource } from "solid-js";
|
||||||
|
import type {OptionsResult, RuntimeResult} from "../../yarn-spinner/runtime/results";
|
||||||
|
import {compile, parseYarn, YarnRunner} from "../../yarn-spinner";
|
||||||
|
import {loadElementSrc, resolvePath} from "../utils/path";
|
||||||
|
import {createStore} from "solid-js/store";
|
||||||
|
import {RunnerOptions} from "../../yarn-spinner/runtime/runner";
|
||||||
|
|
||||||
|
type YarnSpinnerStore = {
|
||||||
|
dialogueHistory: (RuntimeResult | OptionsResult['options'][0])[],
|
||||||
|
currentOptions: OptionsResult | null,
|
||||||
|
isEnded: boolean,
|
||||||
|
runnerInstance: YarnRunner | null,
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createYarnStore(element: HTMLElement, props: RunnerOptions){
|
||||||
|
const [store, setStore] = createStore<YarnSpinnerStore>({
|
||||||
|
dialogueHistory: [],
|
||||||
|
currentOptions: null,
|
||||||
|
isEnded: false,
|
||||||
|
runnerInstance: null
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取文件路径
|
||||||
|
const {articlePath, rawSrc} = loadElementSrc(element);
|
||||||
|
const yarnPaths = (rawSrc || '').split(',')
|
||||||
|
.map((s: string) => resolvePath(articlePath, s.trim()))
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
// 加载 yarn 内容
|
||||||
|
const [yarnContent] = createResource(() => yarnPaths, loadYarnFiles);
|
||||||
|
|
||||||
|
// 创建 runner
|
||||||
|
const createRunner = () => {
|
||||||
|
const content = yarnContent();
|
||||||
|
if (!content) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const ast = parseYarn(content);
|
||||||
|
const program = compile(ast);
|
||||||
|
|
||||||
|
return new YarnRunner(program, props);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to initialize YarnRunner:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function advance(index?: number){
|
||||||
|
const runner = store.runnerInstance;
|
||||||
|
if(!runner) return;
|
||||||
|
if(index === undefined && runner.currentResult?.isDialogueEnd) return;
|
||||||
|
if(runner.currentResult?.type === 'options'){
|
||||||
|
if(index === undefined)return;
|
||||||
|
const option = runner.currentResult.options[index];
|
||||||
|
setStore('dialogueHistory', [...store.dialogueHistory, option]);
|
||||||
|
}
|
||||||
|
runner.advance(index);
|
||||||
|
processRunnerOutput();
|
||||||
|
}
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if(!yarnContent()) return;
|
||||||
|
setStore('runnerInstance', createRunner());
|
||||||
|
requestAnimationFrame(function(){
|
||||||
|
advance();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 处理 runner 输出
|
||||||
|
const processRunnerOutput = () => {
|
||||||
|
const runner = store.runnerInstance;
|
||||||
|
if(!runner)return;
|
||||||
|
|
||||||
|
const result = runner.currentResult;
|
||||||
|
if (!result) return;
|
||||||
|
|
||||||
|
if(result.type === 'options'){
|
||||||
|
setStore('currentOptions', result);
|
||||||
|
}else{
|
||||||
|
setStore('currentOptions', null);
|
||||||
|
}
|
||||||
|
|
||||||
|
setStore('dialogueHistory', [...store.dialogueHistory, result]);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 重新开始
|
||||||
|
const restart = () => {
|
||||||
|
setStore({
|
||||||
|
dialogueHistory: [],
|
||||||
|
currentOptions: null,
|
||||||
|
isEnded: false,
|
||||||
|
runnerInstance: createRunner(),
|
||||||
|
});
|
||||||
|
processRunnerOutput();
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
store,
|
||||||
|
advance,
|
||||||
|
restart,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 缓存已加载的 yarn 文件内容
|
||||||
|
const yarnCache = new Map<string, string>();
|
||||||
|
|
||||||
|
// 加载 yarn 文件内容
|
||||||
|
async function loadYarnFile(path: string): Promise<string> {
|
||||||
|
if (yarnCache.has(path)) {
|
||||||
|
return yarnCache.get(path)!;
|
||||||
|
}
|
||||||
|
const response = await fetch(path);
|
||||||
|
const content = await response.text();
|
||||||
|
yarnCache.set(path, content);
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载多个 yarn 文件并拼接
|
||||||
|
async function loadYarnFiles(paths: string[]): Promise<string> {
|
||||||
|
const contents = await Promise.all(paths.map(path => loadYarnFile(path)));
|
||||||
|
return contents.join('\n');
|
||||||
|
}
|
||||||
|
|
@ -80,52 +80,3 @@ icon{
|
||||||
.col-3{ @apply lg:flex-3; }
|
.col-3{ @apply lg:flex-3; }
|
||||||
.col-4{ @apply lg:flex-4; }
|
.col-4{ @apply lg:flex-4; }
|
||||||
.col-5{ @apply lg:flex-5; }
|
.col-5{ @apply lg:flex-5; }
|
||||||
|
|
||||||
/* yarn-spinner */
|
|
||||||
|
|
||||||
.yarn-spinner {
|
|
||||||
@apply font-sans;
|
|
||||||
}
|
|
||||||
|
|
||||||
.yarn-spinner .dialogue-history {
|
|
||||||
@apply space-y-3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.yarn-spinner .dialogue-entry {
|
|
||||||
@apply leading-relaxed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.yarn-spinner .text-entry {
|
|
||||||
@apply text-gray-800;
|
|
||||||
}
|
|
||||||
|
|
||||||
.yarn-spinner .speaker {
|
|
||||||
@apply text-blue-600 font-semibold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.yarn-spinner .command-entry {
|
|
||||||
@apply text-sm text-gray-500 italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.yarn-spinner .options-entry {
|
|
||||||
@apply mt-2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.yarn-spinner .selected-option {
|
|
||||||
@apply text-gray-600 font-medium;
|
|
||||||
}
|
|
||||||
|
|
||||||
.yarn-spinner .option-button {
|
|
||||||
@apply w-full text-left px-4 py-2.5 bg-blue-50 hover:bg-blue-100
|
|
||||||
rounded border border-blue-200 transition-colors cursor-pointer
|
|
||||||
text-gray-700 hover:text-gray-900;
|
|
||||||
}
|
|
||||||
|
|
||||||
.yarn-spinner .toolbar {
|
|
||||||
@apply bg-gray-50 border-t border-gray-200;
|
|
||||||
}
|
|
||||||
|
|
||||||
.yarn-spinner .restart-button {
|
|
||||||
@apply px-4 py-1.5 text-sm bg-gray-200 hover:bg-gray-300
|
|
||||||
rounded transition-colors cursor-pointer text-gray-700;
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue