-
Notifications
You must be signed in to change notification settings - Fork 423
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
48d2fd1
commit 6dc3e8d
Showing
6 changed files
with
147 additions
and
69 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
// BIG FAT WARNING: to avoid the complexity of npm, this typescript is compiled in the browser | ||
// there's currently no static type checking | ||
|
||
import { marked } from 'https://cdnjs.cloudflare.com/ajax/libs/marked/15.0.0/lib/marked.esm.js' | ||
const convElement = document.getElementById('conversation') | ||
|
||
const promptInput = document.getElementById('prompt-input') as HTMLInputElement | ||
const spinner = document.getElementById('spinner') | ||
|
||
// stream the response and render messages as each chunk is received | ||
// data is sent as newline-delimited JSON | ||
async function onFetchResponse(response: Response): Promise<void> { | ||
let text = '' | ||
let decoder = new TextDecoder() | ||
if (response.ok) { | ||
const reader = response.body.getReader() | ||
while (true) { | ||
const {done, value} = await reader.read() | ||
if (done) { | ||
break | ||
} | ||
text += decoder.decode(value) | ||
addMessages(text) | ||
spinner.classList.remove('active') | ||
} | ||
addMessages(text) | ||
promptInput.disabled = false | ||
promptInput.focus() | ||
} else { | ||
const text = await response.text() | ||
console.error(`Unexpected response: ${response.status}`, {response, text}) | ||
throw new Error(`Unexpected response: ${response.status}`) | ||
} | ||
} | ||
|
||
// The format of messages, this matches pydantic-ai both for brevity and understanding | ||
// in production, you might not want to keep this format all the way to the frontend | ||
interface Message { | ||
role: string | ||
content: string | ||
timestamp: string | ||
} | ||
|
||
// take raw response text and render messages into the `#conversation` element | ||
// Message timestamp is assumed to be a unique identifier of a message, and is used to deduplicate | ||
// hence you can send data about the same message multiple times, and it will be updated | ||
// instead of creating a new message elements | ||
function addMessages(responseText: string) { | ||
const lines = responseText.split('\n') | ||
const messages: Message[] = lines.filter(line => line.length > 1).map(j => JSON.parse(j)) | ||
for (const message of messages) { | ||
// we use the timestamp as a crude element id | ||
const {timestamp, role, content} = message | ||
const id = `msg-${timestamp}` | ||
let msgDiv = document.getElementById(id) | ||
if (!msgDiv) { | ||
msgDiv = document.createElement('div') | ||
msgDiv.id = id | ||
msgDiv.title = `${role} at ${timestamp}` | ||
msgDiv.classList.add('border-top', 'pt-2', role) | ||
convElement.appendChild(msgDiv) | ||
} | ||
msgDiv.innerHTML = marked.parse(content) | ||
} | ||
window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' }) | ||
} | ||
|
||
function onError(error: any) { | ||
console.error(error) | ||
document.getElementById('error').classList.remove('d-none') | ||
document.getElementById('spinner').classList.remove('active') | ||
} | ||
|
||
async function onSubmit(e: SubmitEvent): Promise<void> { | ||
e.preventDefault() | ||
spinner.classList.add('active') | ||
const body = new FormData(e.target as HTMLFormElement) | ||
|
||
promptInput.value = '' | ||
promptInput.disabled = true | ||
|
||
const response = await fetch('/chat/', {method: 'POST', body}) | ||
await onFetchResponse(response) | ||
} | ||
|
||
// call onSubmit when the form is submitted (e.g. user clicks the send button or hits Enter) | ||
document.querySelector('form').addEventListener('submit', (e) => onSubmit(e).catch(onError)) | ||
|
||
// load messages on page load | ||
fetch('/chat/').then(onFetchResponse).catch(onError) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters