-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdemo.html
More file actions
150 lines (124 loc) · 7.47 KB
/
demo.html
File metadata and controls
150 lines (124 loc) · 7.47 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI Event-Driven Storyteller</title>
<style>
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: #f0f2f5; margin: 0; padding: 20px; display: flex; flex-direction: column; align-items: center; }
.container { max-width: 800px; width: 100%; }
.input-area { background: white; padding: 20px; border-radius: 12px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); margin-bottom: 20px; }
textarea { width: 100%; height: 80px; padding: 10px; box-sizing: border-box; border-radius: 8px; border: 1px solid #ccc; font-size: 16px; margin-bottom: 10px; resize: vertical; }
button { background: #1a73e8; color: white; border: none; padding: 12px 24px; border-radius: 6px; font-size: 16px; cursor: pointer; font-weight: bold; transition: background 0.2s; }
button:hover { background: #1557b0; }
button:disabled { background: #a8c7fa; cursor: not-allowed; }
#output { background: white; padding: 30px; border-radius: 12px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); min-height: 200px; }
.scene { margin-bottom: 40px; padding-bottom: 20px; border-bottom: 1px solid #eee; animation: fadeIn 0.8s; }
.scene p { font-size: 1.2rem; line-height: 1.6; color: #333; white-space: pre-wrap; }
.media-container img { width: 100%; max-height: 450px; object-fit: cover; border-radius: 10px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); margin-bottom: 15px; }
.media-container audio { width: 100%; outline: none; margin-top: 15px; }
.loading { font-style: italic; color: #666; display: none; margin-top: 10px; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
</style>
</head>
<body>
<div class="container">
<div class="input-area">
<h2>TaleSpark</h2>
<textarea id="promptInput" placeholder="Enter your story prompt here..."></textarea>
<button id="generateBtn" onclick="startGeneration()">Generate Story</button>
<div id="loadingStatus" class="loading">generating...</div>
</div>
<div id="output"></div>
</div>
<script>
async function startGeneration() {
const promptText = document.getElementById("promptInput").value;
const generateBtn = document.getElementById("generateBtn");
const outputDiv = document.getElementById("output");
const loadingStatus = document.getElementById("loadingStatus");
if (!promptText.trim()) return alert("Please enter a prompt.");
generateBtn.disabled = true;
outputDiv.innerHTML = "";
loadingStatus.style.display = "block";
try {
const response = await fetch("/api/generate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ prompt: promptText })
});
const reader = response.body.getReader();
const decoder = new TextDecoder("utf-8");
let done = false;
let buffer = "";
let currentSceneDiv = null;
let currentTextP = null;
while (!done) {
const { value, done: readerDone } = await reader.read();
done = readerDone;
if (value) {
buffer += decoder.decode(value, { stream: true });
let parts = buffer.split("\n\n");
buffer = parts.pop();
for (let part of parts) {
if (part.startsWith("data: ")) {
try {
let data = JSON.parse(part.substring(6));
// 根据后端传来的事件类型,安全地操作 DOM
if (data.type === "image") {
// 创建新场景和图片容器
currentSceneDiv = document.createElement("div");
currentSceneDiv.className = "scene";
const imgContainer = document.createElement("div");
imgContainer.className = "media-container";
const imgEl = document.createElement("img");
imgEl.src = data.src;
imgContainer.appendChild(imgEl);
currentSceneDiv.appendChild(imgContainer);
// 预留文本
currentTextP = document.createElement("p");
currentSceneDiv.appendChild(currentTextP);
outputDiv.appendChild(currentSceneDiv);
}
else if (data.type === "text") {
if (!currentTextP) {
currentSceneDiv = document.createElement("div");
currentSceneDiv.className = "scene";
currentTextP = document.createElement("p");
currentSceneDiv.appendChild(currentTextP);
outputDiv.appendChild(currentSceneDiv);
}
// 追加文本
currentTextP.appendChild(document.createTextNode(data.chunk));
}
else if (data.type === "audio") {
// 创建音频播放器并追加到当前场景
if (currentSceneDiv) {
const audContainer = document.createElement("div");
audContainer.className = "media-container";
const audEl = document.createElement("audio");
audEl.controls = true;
audEl.src = data.src;
audContainer.appendChild(audEl);
currentSceneDiv.appendChild(audContainer);
}
}
window.scrollTo(0, document.body.scrollHeight);
} catch (e) {
console.error("JSON Parse Error:", e, "Raw data:", part);
}
}
}
}
}
} catch (error) {
console.error("Stream reading error:", error);
outputDiv.innerHTML += `<p style="color:red;">An error occurred during generation.</p>`;
} finally {
generateBtn.disabled = false;
loadingStatus.style.display = "none";
}
}
</script>
</body>
</html>