科學Maker
2026年8月29日 星期六

AI 對話空間:「進階檔案模式」使用過程記錄

作者:
楊于葳
文章封面圖片

輸出檔案這件事是很多人希望有的功能,因應使用者的需求,很快就有了輸出檔案的功能,這是需要額外開啟的功能,使用道具「金花」後,可支援輸出 15 種格式。

「進階檔案模式」支援輸出格式

文件類

檔案類型用途
docxWord 文件(報告、合約、信件等)
pdfPDF 文件(正式文件、簡報列印)
txt純文字檔
mdMarkdown 文件
html網頁檔案
json資料交換格式

簡報類

檔案類型用途
pptxPowerPoint 簡報(可編輯)
ppsxPowerPoint 放映檔(直接播放)

報表類

檔案類型用途
xlsxExcel 試算表(資料表格、統計分析)

技術與其他

檔案類型用途
pyPython 程式碼
csv逗號分隔資料檔
svg向量圖形
midMIDI 音樂檔
odfOpenDocument 公式(MathML)
drawiodraw.io 流程圖/圖表

「進階檔案模式」使用方式

1、使用道具「金花」

因為目前 AI 對話空間的 Fable-talk 模型都是在本地進行,資源有限,所以只有使用道具「金花」的使用者才可以有開啟進階檔案模式的權限。

段落圖片
參與這次的測試,會獲得測試用的金花。

2、開啟「進階檔案模式」

為了避免使用者大失血,這個功能是預設關閉,而且即便是過去曾經開啟過,但離開了對話視窗,再次進來一樣是會被關閉的,所以送出對話之前,可以先確認進階檔案模式是否有開啟。

段落圖片
「進階檔案模式」預設關閉

「進階檔案模式」使用測試過程

注意事項:檔案的下載期限只有24小時,超過期限後就無法下載。

(1)「drawio 流程圖」轉「 pptx 簡報檔」

這是基於「程式語言演進史」為主題的檔案輸出測試過程。最近對 drawio 很感興趣,發現輸出的檔案格式裡面有 drawio,馬上就成為第一個想測試輸出的格式。

在同一個對話裡,先了解如何描述「產生程式語言的演進歷程 drawio 流程圖」這件事情,對話內容是這樣:

然後再請 Fable-talk 產生 drawio 流程圖,這當中有發生檔案打不開的問題,還有後續的解決方式:

段落圖片

可以正常打開後,嘗試把 drawio 流程圖轉換成 pptx 簡報檔:

段落圖片

請 Fable-talk 補充更詳細的資訊,額外做成延伸參考資料:

段落圖片

(2)產生一個 HTML 瑪利歐遊戲

在輸出的 15 種格式裡其中一個是聲音,想試試生成聲音這方面的表現,就請 Fable-talk 製作一個簡易的瑪利歐小遊戲,還真的是有聲音,真的很驚艷!

因為檔案只要超過 24 小時就會無法下載,所以把 HTML 的程式碼直接貼在這裡:

HTML
1<!DOCTYPE html>
2<html lang="zh-Hant">
3<head>
4<meta charset="UTF-8">
5<meta name="viewport" content="width=device-width, initial-scale=1.0">
6<title>🧱 8-bit 平台跳躍遊戲</title>
7<style>
8  * { margin: 0; padding: 0; box-sizing: border-box; }
9  body {
10    background: #1a1a2e;
11    font-family: 'Courier New', monospace;
12    display: flex; flex-direction: column; align-items: center;
13    min-height: 100vh; color: #fff; padding: 20px;
14  }
15  h1 { margin-bottom: 10px; font-size: 20px; letter-spacing: 2px; color: #ffd700; text-shadow: 2px 2px 0 #c0392b; }
16  #gameWrap { position: relative; }
17  canvas {
18    display: block; background: #5c94fc;
19    border-radius: 8px; box-shadow: 0 8px 30px rgba(0,0,0,.5);
20    image-rendering: pixelated;
21  }
22  #hud {
23    display: flex; justify-content: space-between; width: 100%;
24    padding: 10px 14px; background: rgba(0,0,0,.35); border-radius: 8px 8px 0 0;
25    font-size: 16px; font-weight: bold;
26  }
27  #hud span { color: #ffd700; }
28  #hud .label { color: #aaa; }
29  #overlay {
30    position: absolute; inset: 0; display: flex; flex-direction: column;
31    align-items: center; justify-content: center; background: rgba(0,0,0,.55);
32    border-radius: 8px; text-align: center; gap: 12px;
33  }
34  #overlay.hidden { display: none; }
35  #overlay h2 { font-size: 28px; color: #ffd700; letter-spacing: 2px; }
36  #overlay p { font-size: 14px; color: #ddd; max-width: 320px; line-height: 1.6; }
37  button {
38    font-family: inherit; font-size: 18px; font-weight: bold; padding: 12px 28px;
39    background: #e74c3c; color: #fff; border: none; border-radius: 6px;
40    cursor: pointer; letter-spacing: 1px; box-shadow: 0 4px 0 #a93226;
41    transition: transform .05s;
42  }
43  button:hover { transform: translateY(-2px); }
44  button:active { transform: translateY(2px); box-shadow: 0 0 0 #a93226; }
45  #controls { margin-top: 14px; font-size: 13px; color: #bbb; }
46  kbd { background: #444; padding: 2px 7px; border-radius: 4px; font-family: monospace; }
47</style>
48</head>
49<body>
50  <h1>🍄 8-BIT 平台跳躍</h1>
51  <div id="gameWrap">
52    <div id="hud">
53      <span>❤️ <span class="label" id="hudLives">3</span></span>
54      <span>🪙 <span class="label" id="hudCoins">0</span></span>
55      <span><span class="label" id="hudScore">0</span></span>
56    </div>
57    <canvas id="game" width="800" height="450"></canvas>
58    <div id="overlay">
59      <h2 id="overlayTitle">🍄 瑪利歐小冒險</h2>
60      <p id="overlayDesc">收集金幣、踩扁蘑菇怪,抵達終點旗竿!<br>
61        使用 <kbd></kbd> <kbd></kbd> 移動,<kbd>空白</kbd> 跳躍。
62        碰到敵人會扣一條命,踩到牠們頭頂則能消滅。</p>
63      <button id="startBtn">▶ 開始遊戲</button>
64    </div>
65  </div>
66  <div id="controls">
67    <kbd></kbd><kbd></kbd> 移動 &nbsp;|&nbsp; <kbd>空白</kbd> 跳躍 &nbsp;|&nbsp; <kbd>R</kbd> 重新開始
68  </div>
69
70<script>
71// ============ 音效引擎(Web Audio API,合成 8-bit 音效)============
72const AudioEngine = (function() {
73  let ctx = null;
74  let muted = false;
75
76  // 需在使用者互動後才能啟動
77  function ensure() {
78    if (!ctx) {
79      ctx = new (window.AudioContext || window.webkitAudioContext)();
80    }
81    if (ctx.state === 'suspended') ctx.resume();
82    return ctx;
83  }
84
85  // 播放單一方波音符(經典 8-bit 風格)
86  function tone(freq, start, dur, vol = 0.2, type = 'square') {
87    if (!ctx || muted) return;
88    const osc = ctx.createOscillator();
89    const gain = ctx.createGain();
90    osc.type = type;
91    osc.frequency.value = freq;
92    gain.gain.setValueAtTime(0, ctx.currentTime + start);
93    gain.gain.linearRampToValueAtTime(vol, ctx.currentTime + start + 0.01);
94    gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + start + dur);
95    osc.connect(gain);
96    gain.connect(ctx.destination);
97    osc.start(ctx.currentTime + start);
98    osc.stop(ctx.currentTime + start + dur + 0.02);
99  }
100
101  // 雜訊(爆炸/踩扁用)
102  function noise(start, dur, vol = 0.2) {
103    if (!ctx || muted) return;
104    const bufferSize = Math.floor(ctx.sampleRate * dur);
105    const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate);
106    const data = buffer.getChannelData(0);
107    for (let i = 0; i < bufferSize; i++) data[i] = (Math.random() * 2 - 1);
108    const src = ctx.createBufferSource();
109    src.buffer = buffer;
110    const gain = ctx.createGain();
111    gain.gain.setValueAtTime(vol, ctx.currentTime + start);
112    gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + start + dur);
113    src.connect(gain);
114    gain.connect(ctx.destination);
115    src.start(ctx.currentTime + start);
116  }
117
118  function unlock() { ensure(); }
119
120  function setMuted(m) { muted = m; }
121
122  return {
123    unlock,
124    setMuted,
125    jump()   { ensure(); tone(660, 0, 0.15, 0.18); tone(880, 0.08, 0.12, 0.14); },
126    coin()   { ensure(); tone(988, 0, 0.08, 0.18); tone(1319, 0.08, 0.25, 0.16); },          // B5 → E6
127    stomp()  { ensure(); noise(0, 0.15, 0.22); tone(200, 0, 0.12, 0.18, 'sawtooth'); },        // 踩怪
128    powerUp(){ ensure(); tone(523,0,.1,.15); tone(659,0.1,.1,.15); tone(784,0.2,.1,.15); tone(1047,0.3,.3,.18); },
129    hurt()   { ensure(); tone(392, 0, 0.12, 0.2, 'sawtooth'); tone(330, 0.12, 0.12, 0.2, 'sawtooth'); tone(262, 0.24, 0.3, 0.2, 'sawtooth'); },
130    gameOver(){ ensure(); tone(440,0,.2,.18); tone(349,0.2,.2,.18); tone(294,0.4,.2,.18); tone(196,0.6,.6,.22); },
131    win()    { ensure(); const n=[523,659,784,1047,784,659,1047]; n.forEach((f,i)=>tone(f,i*0.12,0.25,.18)); },
132  };
133})();
134
135// ============ 遊戲主程式 ============
136const canvas = document.getElementById('game');
137const ctx = canvas.getContext('2d');
138const W = canvas.width, H = canvas.height;
139const GRAVITY = 0.5;
140const GROUND_Y = 400;
141
142const hudLives = document.getElementById('hudLives');
143const hudCoins = document.getElementById('hudCoins');
144const hudScore = document.getElementById('hudScore');
145const overlay = document.getElementById('overlay');
146const overlayTitle = document.getElementById('overlayTitle');
147const overlayDesc = document.getElementById('overlayDesc');
148const startBtn = document.getElementById('startBtn');
149
150const state = {
151  running: false,
152  started: false,
153  keys: {},
154  player: { x: 80, y: GROUND_Y - 40, w: 32, h: 40, vx: 0, vy: 0, onGround: false, facing: 1 },
155  coins: [
156    { x: 260, y: 300, taken: false },
157    { x: 420, y: 240, taken: false },
158    { x: 520, y: 200, taken: false },
159    { x: 620, y: 320, taken: false },
160  ],
161  enemies: [
162    { x: 350, y: GROUND_Y - 30, w: 32, h: 30, vx: 1.2, alive: true },
163    { x: 550, y: GROUND_Y - 30, w: 32, h: 30, vx: -1.0, alive: true },
164  ],
165  platforms: [
166    { x: 240, y: 330, w: 120, h: 18 },
167    { x: 430, y: 270, w: 120, h: 18 },
168    { x: 620, y: 350, w: 120, h: 18 },
169  ],
170  goal: { x: 740, y: GROUND_Y - 70, w: 20, h: 70 },
171  brick: { x: 470, y: 150, w: 40, h: 40, broken: false },   // 金色方塊(可打碎)
172  lives: 3,
173  coinsCount: 0,
174  score: 0,
175  won: false,
176  invuln: 0,
177};
178
179function resetPlayer() {
180  state.player.x = 80;
181  state.player.y = GROUND_Y - 40;
182  state.player.vx = 0; state.player.vy = 0;
183  state.player.onGround = false;
184}
185
186function resetLevel() {
187  state.coins.forEach(c => c.taken = false);
188  state.enemies[0] = { x: 350, y: GROUND_Y - 30, w: 32, h: 30, vx: 1.2, alive: true };
189  state.enemies[1] = { x: 550, y: GROUND_Y - 30, w: 32, h: 30, vx: -1.0, alive: true };
190  state.brick.broken = false;
191  state.won = false;
192  resetPlayer();
193}
194
195function startGame() {
196  AudioEngine.unlock();
197  state.lives = 3;
198  state.coinsCount = 0;
199  state.score = 0;
200  state.invuln = 0;
201  resetLevel();
202  state.running = true;
203  state.started = true;
204  overlay.classList.add('hidden');
205  updateHud();
206}
207
208function returnToMenu(text, sub) {
209  state.running = false;
210  overlayTitle.textContent = text;
211  overlayDesc.textContent = sub;
212  startBtn.textContent = '⟳ 再玩一次';
213  overlay.classList.remove('hidden');
214}
215
216function updateHud() {
217  hudLives.textContent = state.lives;
218  hudCoins.textContent = state.coinsCount;
219  hudScore.textContent = state.score;
220}
221
222// ---------- 碰撞輔助 ----------
223function rectsOverlap(ax, ay, aw, ah, bx, by, bw, bh) {
224  return ax < bx + bw && ax + aw > bx && ay < by + bh && ay + ah > by;
225}
226
227// ---------- 更新 ----------
228function update() {
229  if (!state.running) return;
230  const p = state.player;
231  const prevY = p.y;
232  const prevX = p.x;
233
234  // 水平移動
235  if (state.keys['ArrowLeft'] || state.keys['a']) { p.vx = -4; p.facing = -1; }
236  else if (state.keys['ArrowRight'] || state.keys['d']) { p.vx = 4; p.facing = 1; }
237  else p.vx = 0;
238  p.x += p.vx;
239
240  // 重力與跳躍
241  if ((state.keys[' '] || state.keys['ArrowUp'] || state.keys['w']) && p.onGround) {
242    p.vy = -11;
243    p.onGround = false;
244    AudioEngine.jump();
245  }
246  p.vy += GRAVITY;
247  p.y += p.vy;
248
249  // 地面碰撞
250  if (p.y + p.h >= GROUND_Y) {
251    if (prevY + p.h <= GROUND_Y) { p.onGround = true; }
252    p.y = GROUND_Y - p.h; p.vy = 0; p.onGround = true;
253  }
254
255  // 平台碰撞(只從上方)
256  for (const pf of state.platforms) {
257    if (rectsOverlap(p.x, p.y, p.w, p.h, pf.x, pf.y, pf.w, pf.h) && p.vy > 0 && prevY + p.h <= pf.y + 4) {
258      p.y = pf.y - p.h;
259      p.vy = 0;
260      p.onGround = true;
261    }
262  }
263
264  // 金色方塊:從下方撞破
265  if (!state.brick.broken &&
266      rectsOverlap(p.x, p.y, p.w, p.h, state.brick.x, state.brick.y, state.brick.w, state.brick.h) &&
267      p.vy < 0) {
268    state.brick.broken = true;
269    state.coinsCount++;
270    state.score += 100;
271    AudioEngine.powerUp();
272    updateHud();
273    p.vy = 5;
274  }
275
276  // 金幣收集
277  for (const c of state.coins) {
278    if (!c.taken && rectsOverlap(p.x, p.y, p.w, p.h, c.x, c.y, 22, 22)) {
279      c.taken = true;
280      state.coinsCount++;
281      state.score += 200;
282      AudioEngine.coin();
283      updateHud();
284    }
285  }
286
287  // 敵人移動與平台邊緣反彈
288  for (const e of state.enemies) {
289    if (!e.alive) continue;
290    e.x += e.vx;
291    if (e.x <= 250) e.vx = Math.abs(e.vx);
292    if (e.x + e.w >= 720) e.vx = -Math.abs(e.vx);
293
294    // 踩怪
295    if (rectsOverlap(p.x, p.y, p.w, p.h, e.x, e.y, e.w, e.h)) {
296      if (p.vy > 0 && prevY + p.h <= e.y + 8) {
297        e.alive = false;
298        p.vy = -9;
299        state.score += 300;
300        AudioEngine.stomp();
301        updateHud();
302      } else if (state.invuln <= 0) {
303        damage();
304      }
305    }
306  }
307
308  // 抵達終點
309  if (rectsOverlap(p.x, p.y, p.w, p.h, state.goal.x, state.goal.y, state.goal.w, state.goal.h) && !state.won) {
310    state.won = true;
311    state.score += 500;
312    AudioEngine.win();
313    updateHud();
314    setTimeout(() => {
315      if (state.started) returnToMenu('🏁 你過關了!', '得分:' + state.score + ' 金幣:' + state.coinsCount +
316        '<br>還有榮譽加分 +500 分。再來一局?');
317    }, 700);
318    state.running = false;
319  }
320
321  // 無敵時間
322  if (state.invuln > 0) state.invuln--;
323
324  // 防止超出右界
325  if (p.x > W - p.w) p.x = W - p.w;
326  if (p.x < 0) p.x = 0;
327}
328
329function damage() {
330  state.lives--;
331  AudioEngine.hurt();
332  state.invuln = 60;
333  updateHud();
334  if (state.lives <= 0) {
335    AudioEngine.gameOver();
336    setTimeout(() => returnToMenu('💀 遊戲結束', '你踩扁了蘑菇,但用光了生命。<br>再挑戰一次吧!'), 600);
337    state.running = false;
338    resetPlayer();
339  } else {
340    resetPlayer();
341  }
342}
343
344// ---------- 繪製 ----------
345function draw() {
346  // 天空
347  ctx.fillStyle = '#5c94fc';
348  ctx.fillRect(0, 0, W, H);
349  // 雲朵
350  ctx.fillStyle = 'rgba(255,255,255,0.8)';
351  ctx.fillRect(120, 80, 60, 20); ctx.fillRect(140, 65, 20, 20);
352  ctx.fillRect(500, 120, 60, 20); ctx.fillRect(520, 105, 20, 20);
353  ctx.fillRect(370, 40, 40, 14); ctx.fillRect(385, 30, 14, 14);
354
355  // 地面(磚塊)
356  ctx.fillStyle = '#c0392b';
357  ctx.fillRect(0, GROUND_Y, W, H - GROUND_Y);
358  ctx.fillStyle = '#d35400';
359  ctx.fillRect(0, GROUND_Y, W, 8);
360  // 磚線
361  ctx.strokeStyle = 'rgba(0,0,0,0.25)';
362  ctx.lineWidth = 1;
363  for (let x = 0; x < W; x += 32) {
364    for (let y = GROUND_Y; y < H; y += 16) {
365      ctx.strokeRect(x, y, 32, 16);
366    }
367  }
368
369  // 平台
370  ctx.fillStyle = '#8e44ad';
371  for (const pf of state.platforms) ctx.fillRect(pf.x, pf.y, pf.w, pf.h);
372
373  // 金色方塊
374  if (!state.brick.broken) {
375    ctx.fillStyle = '#f1c40f';
376    ctx.strokeStyle = '#b7950b';
377    ctx.lineWidth = 2;
378    ctx.fillRect(state.brick.x, state.brick.y, state.brick.w, state.brick.h);
379    ctx.strokeRect(state.brick.x + 2, state.brick.y + 2, state.brick.w - 4, state.brick.h - 4);
380    ctx.font = 'bold 18px monospace';
381    ctx.textAlign = 'center';
382    ctx.fillStyle = '#8d6e00';
383    ctx.fillText('?', state.brick.x + state.brick.w / 2, state.brick.y + 28);
384  }
385
386  // 金幣(動畫閃爍)
387  for (const c of state.coins) {
388    if (c.taken) continue;
389    const flicker = Math.sin(state.frame * 0.2) > 0;
390    ctx.fillStyle = flicker ? '#ffd700' : '#f39c12';
391    ctx.beginPath();
392    ctx.arc(c.x + 11, c.y + 11, 11, 0, Math.PI * 2);
393    ctx.fill();
394    ctx.strokeStyle = '#e67e22'; ctx.lineWidth = 2;
395    ctx.beginPath(); ctx.arc(c.x + 11, c.y + 11, 11, 0, Math.PI * 2); ctx.stroke();
396  }
397
398  // 敵人(蘑菇怪 Goomba)
399  for (const e of state.enemies) {
400    if (!e.alive) continue;
401    ctx.fillStyle = '#8b4513';
402    ctx.strokeStyle = '#5d2e0d';
403    ctx.fillRect(e.x, e.y, e.w, e.h);
404    ctx.strokeRect(e.x, e.y, e.w, e.h);
405    ctx.fillStyle = '#fff';
406    ctx.fillRect(e.x + 6, e.y + 8, 6, 8);
407    ctx.fillRect(e.x + 20, e.y + 8, 6, 8);
408    ctx.fillStyle = '#000';
409    ctx.fillRect(e.x + 8, e.y + 11, 3, 3);
410    ctx.fillRect(e.x + 22, e.y + 11, 3, 3);
411    ctx.fillStyle = '#2c3e50';
412    ctx.fillRect(e.x + 2, e.y + e.h - 4, e.w - 4, 4);
413  }
414
415  // 玩家(簡化瑪利歐)
416  const p = state.player;
417  if (state.invuln > 0 && Math.floor(state.frame / 6) % 2 === 0) {
418    // 受傷閃爍:跳過繪製
419  } else {
420    ctx.save();
421    ctx.translate(p.x + p.w / 2, p.y + p.h / 2);
422    ctx.scale(p.facing, 1);
423    // 帽子
424    ctx.fillStyle = '#e74c3c';
425    ctx.fillRect(-14, -p.h / 2 - 10, 28, 12);
426    // 臉與身體
427    ctx.fillStyle = '#d8a06a';
428    ctx.fillRect(-12, -p.h / 2 + 2, 24, 14);
429    ctx.fillStyle = '#3498db';
430    ctx.fillRect(-14, -p.h / 2 + 16, 28, 10);
431    ctx.fillStyle = '#e74c3c';
432    ctx.fillRect(-14, -p.h / 2 + 26, 28, 10);
433    // 眼睛
434    ctx.fillStyle = '#000';
435    ctx.fillRect(6, -p.h / 2 + 4, 4, 5);
436    ctx.fillRect(-2, -p.h / 2 + 4, 4, 5);
437    ctx.restore();
438  }
439
440  // 終點旗竿
441  ctx.fillStyle = '#ecf0f1';
442  ctx.fillRect(state.goal.x + 7, state.goal.y, 6, state.goal.h);
443  if (state.won) {
444    ctx.fillStyle = '#f1c40f';
445    ctx.fillRect(state.goal.x + 13, state.goal.y, 16, 10);
446  } else {
447    ctx.fillStyle = '#e74c3c';
448    ctx.fillRect(state.goal.x + 13, state.goal.y, 16, 10);
449  }
450
451  state.frame++;
452}
453
454// ---------- 主迴圈 ----------
455function loop() {
456  update();
457  draw();
458  requestAnimationFrame(loop);
459}
460
461// ---------- 輸入 ----------
462window.addEventListener('keydown', (e) => {
463  if ([' ','ArrowLeft','ArrowRight','ArrowUp','ArrowDown'].includes(e.key)) e.preventDefault();
464  state.keys[e.key] = true;
465  if (e.key.toLowerCase() === 'r') startGame();
466});
467window.addEventListener('keyup', (e) => { state.keys[e.key] = false; });
468startBtn.addEventListener('click', startGame);
469
470// 啟動
471state.frame = 0;
472loop();
473</script>
474</body>
475</html>
476
段落圖片段落圖片