Skip to main content
Unlisted page
This page is unlisted. Search engines will not index it, and only users having a direct link can access it.

Use the Thinkfree chat UI with your own loop

Editor SDK and an Office server are required

The chat UI is served by the Office server that opens the document. Your page loads it in an iframe and drives it through createChatUiBridge() from @thinkfree.dev/tfo-ai-sdk/chat-ui. Document tools still come from the Office toolkit. Complete the quickstart first.

The Thinkfree chat UI shows progress chips, notification cards, choice prompts, edit-plan approval, and attachments. With the chat UI bridge you keep that UI and bring only two things: your tool loop and your model connection. The bridge exposes the eight chat UI tools to the model, renders the model's output in the UI, and reports user actions (choices, approvals, stop) back to your loop.

PieceProvided by
Chat UI (React, served by the Office server)Thinkfree
Document tools, prompt rules, receiptsOffice toolkit (@thinkfree.dev/tfo-sdk)
Chat UI tools, prompt rules, turn state, handshakeChat UI bridge (@thinkfree.dev/tfo-ai-sdk/chat-ui)
Tool loop, model connection, relayYour application

1. Load the chat UI in external mode​

Point an iframe at the chat UI page of the Office server with mode=external. The page then renders the UI only and talks to your window instead of starting its own agent.

<iframe id="chat-frame" title="Thinkfree chat UI"
src="https://office.example.com/webword/commonFrame/js/addIn/tfoAiAssistant/index.html?mode=external&brainOrigin=https%3A%2F%2Fapp.example.com&module=word&lang=en"></iframe>
Query parameterRequiredMeaning
mode=externalyesRender the UI only; the agent lives outside this iframe
brainOriginyesExact origin of the page that creates the bridge (your page). * is rejected
brainWindow=openernoSend to window.opener instead of window.parent when you open the UI with window.open()
modulenoword, cell, or show - selects the welcome screen and quick actions
langnoUI language, for example ko_KR or en

2. Create the bridge​

import { createOfficeToolkit } from "@thinkfree.dev/tfo-sdk";
import { createChatUiBridge } from "@thinkfree.dev/tfo-ai-sdk/chat-ui";

const toolkit = await createOfficeToolkit(word, { provider: "anthropic", preset: "core" });
const chatFrame = document.querySelector("#chat-frame");

const chat = createChatUiBridge({
peerWindow: chatFrame.contentWindow,
peerOrigin: "https://office.example.com", // exact origin of the chat UI, never "*"
provider: "anthropic", // same wire shape as toolkit.tools
language: { code: "en", name: "English" }, // language named in the prompt rules
listTools: () => toolkit.catalog, // answers the "/tools" command locally
onUserInput: runTurn, // your loop - see step 3
onAbort: () => console.log("user pressed stop"),
onSettingsChange: (settings, ctx) => { /* ctx.systemPrompt changed; use it on the next turn */ },
});
Handle memberUse it for
chat.toolsThe eight chat UI tools in the provider wire shape. Send them together with toolkit.tools
chat.systemPromptRules for the chat UI tools (edit-accept or auto-proceed mode, developer mode). Append it to toolkit.systemPrompt
chat.handles(name)true when a tool call belongs to the chat UI; route it to chat.dispatch, everything else to toolkit.dispatch
chat.dispatch(name, args)Runs a chat UI tool and resolves with a receipt. Interactive cards resolve when the user answers
chat.notify(text)Renders the model's final text as a card. Skipped when the turn already reported through a card
chat.settingsCurrent assistant settings: mode (edit-accept or auto-proceed) and developerMode
chat.destroy()Removes the listener and closes pending cards

3. Run your loop on each user input​

onUserInput receives the text and attachments the user sent and a turn context. Return the model's final text and the bridge renders it; throw and the bridge shows an error card. turn.signal aborts when the user presses stop.

async function runTurn({ text, attachments }, turn) {
const system = `${toolkit.systemPrompt}\n\n${chat.systemPrompt}`;
const tools = [...toolkit.tools, ...chat.tools];
const history = [{ role: "user", content: text }];
for (let step = 0; step < 12; step++) {
if (turn.signal.aborted) return "";
const res = await createMessage({ system, messages: history, tools }); // see the quickstart
history.push({ role: "assistant", content: res.content });
if (res.stop_reason !== "tool_use") {
return res.content.filter((b) => b.type === "text").map((b) => b.text).join("\n");
}
const results = [];
for (const call of res.content.filter((b) => b.type === "tool_use")) {
const receipt = chat.handles(call.name)
? await chat.dispatch(call.name, call.input)
: await toolkit.dispatch(call.name, call.input);
results.push({ type: "tool_result", tool_use_id: call.id,
content: JSON.stringify(receipt), is_error: !receipt.success });
}
history.push({ role: "user", content: results });
}
return "";
}

Both dispatch functions return receipts, so the model reads one result shape for document tools and chat UI tools. When the model calls show_request_input or show_edit_plan_list, chat.dispatch waits until the user chooses, accepts, or closes the card, and the receipt text tells the model what happened. In edit-accept mode (the default) the prompt rules tell the model to show an edit plan and wait for approval before it calls a document tool; your loop does not need to add an approval step for that path.

4. Handshake and lifecycle​

  • Handshake. The bridge sends tfo-ai:hello when created, and the UI answers tfo-ai:ready with its protocol version; the UI also sends ready when it loads. Either side may come up first. Messages the bridge sends before ready are queued. If the versions differ, the UI shows an error card and the bridge stops; onProtocolMismatch fires.
  • Turn state. The bridge marks the turn as running when onUserInput starts and as finished when it settles, so the UI shows the stop button and queues further input meanwhile.
  • Stop. The user's stop aborts turn.signal, calls onAbort, and closes waiting cards so that pending chat.dispatch calls resolve. A result or error that arrives after the stop is dropped.
  • Settings. When the user changes the assistant settings in the UI, onSettingsChange(settings, { systemPrompt, notice }) fires. Use the new systemPrompt from the next turn; notice is a short message you may add to the conversation so the model notices the change.
  • Cleanup. Call chat.destroy() when you remove the iframe.

Protocol reference (v1)​

Every message is window.postMessage({ type, payload }, origin) with an exact origin on both sides.

MessageDirectionPayload
tfo-ai:readyUI → bridge{ protocolVersion: 1, capabilities?: string[] }
tfo-ai:hellobridge → UI{ protocolVersion: 1 }
tfo-ai:user-inputUI → bridge{ text, attachments?: [{ fileName, mimeType, size, base64 }] }
tfo-ai:user-actionUI → bridge{ componentId, action: "select" | "select-custom" | "accept-all" | "close", value? }
tfo-ai:set-assistant-settingsUI → bridge{ settings: { mode, developerMode } }
tfo-ai:abort-turn, tfo-ai:clear-conversationUI → bridgenone
tfo-ai:component-add / -update / -removebridge → UI{ id, kind, data } / { id, data } / { id }
tfo-ai:turn-statebridge → UI{ inProgress }
tfo-ai:trigger-inputbridge → UI{ text } - inject an utterance as if the user typed it

Component kinds are progress-status, notification, request-input, edit-plan-list, and tool-call-info; the chat UI tools (show_progress_status, update_progress_status, show_notification, show_request_input, show_edit_plan_list, update_edit_plan_list, show_tool_call_info, remove_component) produce them. You do not need to send component messages yourself; the bridge does it when the model calls a chat UI tool.

Limits in this release​

  • Attachments are passed to onUserInput as base64 and are not uploaded anywhere by the bridge. Text and image attachments are practical; Office binary attachments (such as .pptx) are not bridged in external mode.
  • The chat UI is served by the Office server and must be loaded from there; hosting it on your own domain is not supported.
  • The prompt rules are written in Korean with the UI language named inside; the model answers in the user's language.