Document Canvas

Configuration

Set your API provider and key here. Any OpenAI-compatible API that supports function calling will work.

Providers

ProviderDefault ModelAPI Key
OpenAIgpt-5.2Required
Anthropic (Claude)claude-sonnet-4-20250514Required
Groqllama-3.3-70b-versatileRequired
Together AILlama-3.3-70B-Instruct-TurboRequired
Mistralmistral-small-latestRequired
OpenRouteropenai/gpt-4oRequired
Ollama (local)llama3.1Not needed
LM Studio (local)local-modelNot needed

System Prompt

This prompt is sent to the LLM with every request. Edit it to change the assistant's behavior.

Settings are stored as cookies on your local machine (prefixed te_). Your API key is never sent anywhere except the API endpoint you configure.

How It Works

This example connects two components — a QuikChat widget and a QuikdownEditor — through an LLM that uses function calling to bridge them. The user types natural-language commands in the chat; the LLM decides which editing operations to perform.

Tool Reference

Seven tools are registered with the LLM via the OpenAI function calling API. The LLM decides which tools to call and in what order.

ToolParametersDescription
read_editor none Returns the full markdown content of the editor. The LLM should always call this first before making changes.
write_editor content (string) Replaces the entire editor content. Used for large rewrites, translations, or restructuring.
replace_text find, replace (strings) Finds the first occurrence of an exact string and replaces it. Best for small, targeted edits.
extract_text start_line, end_line (integers) Returns lines in range (1-based, inclusive) without modifying the document. Useful for quoting or analyzing a specific section.
get_stats none Returns JSON with characters, words, lines, and paragraphs counts.
undo none Undoes the last edit and returns the resulting content.
redo none Redoes a previously undone edit and returns the resulting content.

API Configuration

ParameterValueWhy
streamfalseTool-call JSON must be parsed as complete objects — streaming would fragment them.
temperature0.3Editing tasks benefit from more deterministic output.
Conversation historySeparate JS arrayOpenAI tool calling requires tool_calls and tool_call_id fields in the message history, which historyGet() doesn't preserve.
Max tool-call rounds10Safety limit to prevent infinite loops if the LLM keeps calling tools.

Architecture

1. UserTypes a command in chat
(e.g. "summarize this")
2. LLMReceives message + tool definitions, decides to call tools
3. Tool ExecBrowser runs tool locally
(read/write/replace editor)
4. ResultTool output sent back to LLM as next message
5. ResponseLLM replies with summary of changes

Steps 2–4 repeat if the LLM calls multiple tools (e.g. read_editor then write_editor). The loop runs up to 10 rounds per user message.

Tool-Call Loop (Non-Streaming)

Streaming is disabled (stream: false) because tool-call JSON in the response must be parsed as complete objects. The loop works as follows:

async function handleUserInput(chatInstance, userInput) {
  // 1. Show user message, disable input
  chatInstance.messageAddNew(userInput, 'user', 'right');

  // 2. Push to conversation history (separate from chat widget)
  conversationMessages.push({ role: 'user', content: userInput });

  // 3. Build messages = [system_prompt, ...conversationMessages]
  const messages = [{ role: 'system', content: systemPrompt }, ...conversationMessages];

  // 4. Tool-call loop (max 10 rounds)
  for (let round = 0; round < 10; round++) {
    const data = await fetch(baseUrl, {
      method: 'POST',
      headers: { 'Authorization': 'Bearer ' + apiKey, ... },
      body: JSON.stringify({ model, messages, tools, temperature: 0.3, stream: false })
    }).then(r => r.json());

    const choice = data.choices[0];

    // If the model called tools, execute them and continue the loop
    if (choice.message.tool_calls) {
      for (const tc of choice.message.tool_calls) {
        const result = executeTool(tc.function.name, JSON.parse(tc.function.arguments));
        messages.push({ role: 'tool', tool_call_id: tc.id, content: result });
      }
      continue;
    }

    // Otherwise, display the final text response and break
    chatInstance.messageAddNew(choice.message.content, 'bot', 'left');
    break;
  }
}

Hidden Tool-Call Messages

Each tool call and result is added to the chat with visible: false and tagged 'tool-call'. This keeps the conversation clean while preserving a full audit trail. The Show Tool Calls button toggles visibility using messageSetVisibleByTag():

// Add hidden tool-call message
chatInstance.messageAddFull({
  content: 'Tool: read_editor\nResult: ...',
  role: 'tool',
  visible: false,
  tags: ['tool-call']
});

// Toggle all tool-call messages on/off
chat.messageSetVisibleByTag('tool-call', true);  // show
chat.messageSetVisibleByTag('tool-call', false); // hide

Conversation History

The conversation is tracked in a separate JavaScript array rather than using historyGet(). This is necessary because the OpenAI tool-calling protocol requires tool_calls objects on assistant messages and tool_call_id fields on tool result messages — metadata that QuikChat's history format doesn't capture.

Components Used

ComponentBuildRole
QuikChat quikchat-md.umd.min.js Chat widget with markdown rendering (LLM responses may contain markdown). Handles user input, message display, and tool-call visibility toggling.
QuikdownEditor quikdown_edit.umd.min.js (CDN) Split-mode markdown editor. The LLM reads and writes its content via the getMarkdown() and setMarkdown() API.

Try These Commands