From 4edb27fc89dca22346beb6b3d9602d3a0eb3bb25 Mon Sep 17 00:00:00 2001 From: "Marco S. Casalaina" Date: Wed, 15 May 2024 14:11:59 -0700 Subject: [PATCH] Convert openai-asst-webpage-with-functions-js to TypeScript Converts the OpenAI Assistants Webpage project from JavaScript to TypeScript, including updating dependencies and build configurations. - Renames and migrates JavaScript files to TypeScript, ensuring type safety and leveraging TypeScript features for better code management. - Updates the `package.json` to include TypeScript-specific dependencies and modifies scripts for Vite, replacing Webpack with a more modern build tool. - Introduces a `tsconfig.json` for TypeScript compiler options and a `vite-env.d.ts` for Vite environment types, enhancing the development environment. - Adjusts the HTML template to reference the new TypeScript entry point and updates the favicon link for consistency. --- For more details, open the [Copilot Workspace session](https://copilot-workspace.githubnext.com/Azure/azure-ai-cli?shareId=b91c5c07-f488-46dd-b905-8cc14e0ec2b7). --- .../src/script.js | 505 ----------------- .../webpack.config.js | 14 - .../README.md | 14 +- .../_.json | 4 +- .../index.html | 15 +- .../package.json | 11 +- .../src/FunctionFactory.ts} | 12 +- .../src/OpenAIAssistantsCustomFunctions.ts} | 10 +- ...penAIAssistantsFunctionsStreamingClass.ts} | 26 +- .../src/index.ts | 506 ++++++++++++++++++ .../src/vite-env.d.ts | 16 + .../style.css | 0 .../tsconfig.json | 38 ++ .../vite.config.ts | 14 + 14 files changed, 622 insertions(+), 563 deletions(-) delete mode 100644 src/ai/.x/templates/openai-asst-webpage-with-functions-js/src/script.js delete mode 100644 src/ai/.x/templates/openai-asst-webpage-with-functions-js/webpack.config.js rename src/ai/.x/templates/{openai-asst-webpage-with-functions-js => openai-asst-webpage-with-functions-ts}/README.md (71%) rename src/ai/.x/templates/{openai-asst-webpage-with-functions-js => openai-asst-webpage-with-functions-ts}/_.json (90%) rename src/ai/.x/templates/{openai-asst-webpage-with-functions-js => openai-asst-webpage-with-functions-ts}/index.html (84%) rename src/ai/.x/templates/{openai-asst-webpage-with-functions-js => openai-asst-webpage-with-functions-ts}/package.json (72%) rename src/ai/.x/templates/{openai-asst-webpage-with-functions-js/src/FunctionFactory.js => openai-asst-webpage-with-functions-ts/src/FunctionFactory.ts} (71%) rename src/ai/.x/templates/{openai-asst-webpage-with-functions-js/src/OpenAIAssistantsCustomFunctions.js => openai-asst-webpage-with-functions-ts/src/OpenAIAssistantsCustomFunctions.ts} (86%) rename src/ai/.x/templates/{openai-asst-webpage-with-functions-js/src/OpenAIAssistantsFunctionsStreamingClass.js => openai-asst-webpage-with-functions-ts/src/OpenAIAssistantsFunctionsStreamingClass.ts} (76%) create mode 100644 src/ai/.x/templates/openai-asst-webpage-with-functions-ts/src/index.ts create mode 100644 src/ai/.x/templates/openai-asst-webpage-with-functions-ts/src/vite-env.d.ts rename src/ai/.x/templates/{openai-asst-webpage-with-functions-js => openai-asst-webpage-with-functions-ts}/style.css (100%) create mode 100644 src/ai/.x/templates/openai-asst-webpage-with-functions-ts/tsconfig.json create mode 100644 src/ai/.x/templates/openai-asst-webpage-with-functions-ts/vite.config.ts diff --git a/src/ai/.x/templates/openai-asst-webpage-with-functions-js/src/script.js b/src/ai/.x/templates/openai-asst-webpage-with-functions-js/src/script.js deleted file mode 100644 index bc40d8f4..00000000 --- a/src/ai/.x/templates/openai-asst-webpage-with-functions-js/src/script.js +++ /dev/null @@ -1,505 +0,0 @@ -const marked = require("marked"); -const hljs = require("highlight.js"); -const { OpenAI } = require('openai'); - -const { factory } = require("./OpenAIAssistantsCustomFunctions"); -const { {ClassName} } = require("./OpenAIAssistantsFunctionsStreamingClass"); - -let assistant; -async function assistantInit(threadId = null) { - - // Which assistant, which thread? - const ASSISTANT_ID = process.env.ASSISTANT_ID ?? ""; - - {{@include openai.asst.or.chat.create.openai.node.js}} - - // Create the assistants streaming helper class instance - assistant = new {ClassName}(ASSISTANT_ID, factory, openai); - - await assistantCreateOrRetrieveThread(threadId); -} - -async function assistantProcessInput(userInput) { - const blackVerticalRectangle = '\u25AE'; // Black vertical rectangle ('▮') to simulate an insertion point - - let newMessage = chatPanelAppendMessage('computer', blackVerticalRectangle); - let completeResponse = ""; - - await assistant.getResponse(userInput, function (response) { - let atBottomBeforeUpdate = chatPanelIsScrollAtBottom(); - - completeResponse += response; - let withEnding = `${completeResponse}${blackVerticalRectangle}`; - let asHtml = markdownToHtml(withEnding); - - if (asHtml !== undefined) { - newMessage.innerHTML = asHtml; - - if (atBottomBeforeUpdate) { - chatPanelScrollToBottom(); - } - } - }); - - newMessage.innerHTML = markdownToHtml(completeResponse) || completeResponse.replace(/\n/g, '
'); - chatPanel.scrollTop = chatPanel.scrollHeight; - - await threadItemsCheckIfUpdatesNeeded(userInput, completeResponse); -} - -async function assistantCreateOrRetrieveThread(threadId = null) { - - if (threadId === null) { - await assistant.createThread() - } else { - await assistant.retrieveThread(threadId); - await assistant.getThreadMessages((role, content) => { - let html = markdownToHtml(content) || content.replace(/\n/g, '
'); - role = role === 'user' ? 'user' : 'computer'; - console.log(`role: ${role}, content: ${content}`); - chatPanelAppendMessage(role, html); - }); - } -} - -function chatPanelGetElement() { - return document.getElementById("chatPanel"); -} - -function chatPanelAppendMessage(sender, message) { - logoHide(); - - let messageContent = document.createElement("p"); - messageContent.className = "message-content"; - messageContent.innerHTML = message; - - let messageAuthor = document.createElement("p"); - messageAuthor.className = "message-author"; - messageAuthor.innerHTML = sender == "user" ? "You" : "Assistant"; - - let divContainingBoth = document.createElement("div"); - divContainingBoth.className = sender === "user" ? "user" : "computer"; - divContainingBoth.appendChild(messageAuthor); - divContainingBoth.appendChild(messageContent); - - let chatPanel = chatPanelGetElement(); - chatPanel.appendChild(divContainingBoth); - chatPanelScrollToBottom(); - - return messageContent; -} - -function chatPanelIsScrollAtBottom() { - let chatPanel = chatPanelGetElement(); - let atBottom = Math.abs(chatPanel.scrollHeight - chatPanel.clientHeight - chatPanel.scrollTop) < 1; - return atBottom; -} - -function chatPanelScrollToBottom() { - let chatPanel = chatPanelGetElement(); - chatPanel.scrollTop = chatPanel.scrollHeight; -} - -function chatPanelClear() { - let chatPanel = chatPanelGetElement(); - chatPanel.innerHTML = ''; -} - -function logoGetElement() { - return document.getElementById("logo"); -} - -function logoShow() { - let logo = logoGetElement(); - logo.style.display = "block"; -} - -function logoHide() { - let logo = logoGetElement(); - logo.style.display = "none"; -} - -function markdownInit() { - marked.setOptions({ - highlight: function (code, lang) { - let hl = lang === undefined || lang === '' - ? hljs.highlightAuto(code).value - : hljs.highlight(lang, code).value; - return `
${hl}
`; - } - }); -} - -function markdownToHtml(markdownText) { - try { - return marked.parse(markdownText); - } - catch (error) { - return undefined; - } -} - -function themeInit() { - let currentTheme = localStorage.getItem('theme'); - if (currentTheme === 'dark') { - themeSetDark(); - } - else if (currentTheme === 'light') { - themeSetLight(); - } - toggleThemeButtonInit(); -} - -function themeIsLight() { - return document.body.classList.contains("light-theme"); -} - -function themeIsDark() { - return !themeIsLight(); -} - -function toggleTheme() { - if (themeIsLight()) { - themeSetDark(); - } else { - themeSetLight(); - } -} - -function themeSetLight() { - if (!themeIsLight()) { - document.body.classList.add("light-theme"); - localStorage.setItem('theme', 'light'); - - let iconElement = toggleThemeButtonGetElement().children[0]; - iconElement.classList.remove("fa-toggle-on"); - iconElement.classList.add("fa-toggle-off"); - } -} - -function themeSetDark() { - if (!themeIsDark()) { - document.body.classList.remove("light-theme"); - localStorage.setItem('theme', 'dark'); - - let iconElement = toggleThemeButtonGetElement().children[0]; - iconElement.classList.remove("fa-toggle-off"); - iconElement.classList.add("fa-toggle-on"); - } -} - -function toggleThemeButtonGetElement() { - return document.getElementById("toggleThemeButton"); -} - -function toggleThemeButtonInit() { - let buttonElement = toggleThemeButtonGetElement(); - buttonElement.addEventListener("click", toggleTheme); - buttonElement.addEventListener('keydown', toggleThemeButtonHandleKeyDown()); -} - -function toggleThemeButtonHandleKeyDown() { - return function (event) { - if (event.code === 'Enter' || event.code === 'Space') { - toggleTheme(); - } - }; -} - -const titleUntitled = 'Untitled'; - -function ThreadItem(id, created, metadata) { - this.id = id; - this.created = created; - this.metadata = metadata; -} - -function threadItemIsUntitled(item) { - return item.metadata === titleUntitled; -} - -async function threadItemsCheckIfUpdatesNeeded(userInput, computerResponse) { - let items = threadItemsGet(); - threadItemsCheckMoveOrAdd(items); - - await threadItemsSetTitleIfUntitled(items, userInput, computerResponse); -} - -function threadItemsCheckMoveOrAdd(items) { - threadItemsCheckMoveTop(items, assistant.thread.id); - threadItemsCheckAddNew(items, assistant.thread.id); -} - -function threadItemsCheckMoveTop(items, threadId) { - let item = items.find(item => item.id === threadId); - if (item) { - threadItemsMoveTop(items, item); - } -} - -function threadItemsMoveTop(items, item) { - var index = items.indexOf(item); - if (index !== -1) { - items.splice(index, 1); - } - item.created = Math.floor(Date.now() / 1000); - items.unshift(item); - localStorage.setItem('threadItems', JSON.stringify(items)); - threadPanelPopulate(items); -} - -function threadItemsCheckAddNew(items, threadId) { - if (items.length === 0 || items[0].id !== threadId) { - threadItemsAddNew(items, new ThreadItem(threadId, Math.floor(Date.now() / 1000), titleUntitled)); - } -} - -function threadItemsAddNew(items, newItem) { - items.unshift(newItem); - localStorage.setItem('threadItems', JSON.stringify(items)); - threadPanelPopulate(items); -} - -function threadItemsGet() { - const threadItemsString = localStorage.getItem('threadItems'); - if (threadItemsString) { - return JSON.parse(threadItemsString); - } else { - return []; - } -} - -function threadItemsLoadFakeData() { - const now = new Date(); - const yesterday = new Date(new Date().setDate(now.getDate() - 1)); - const thirtyDaysAgo = new Date(new Date().setDate(now.getDate() - 30)); - - const fakeThreadItems = [ - new ThreadItem('thread_XTqDWuGXPjsddI1xctQ2ZD4B', Math.floor(now / 1000), 'Skeleton joke'), - new ThreadItem('thread_wzmGKFC22PKKcvoDs2zrYLD7', Math.floor(yesterday / 1000), 'Why is the sky blue?'), - new ThreadItem('thread_IAxIrq4YJmFflA1fraw7iEcI', Math.floor(yesterday / 1000), 'Hello world in C#'), - new ThreadItem('thread_RAgQWZFf3B3MWjVIpSO6JiRi', Math.floor(thirtyDaysAgo / 1000), 'Thread stuff'), - ]; - return fakeThreadItems; -} - -function threadItemsGetGroupName(timestamp) { - const now = new Date(); - const itemDate = new Date(timestamp * 1000); - const isToday = itemDate.toDateString() === now.toDateString(); - const isYesterday = itemDate.toDateString() === new Date(new Date().setDate(now.getDate() - 1)).toDateString(); - const isThisWeek = itemDate > new Date(new Date().setDate(now.getDate() - 7)); - const isThisYear = itemDate.getFullYear() === now.getFullYear(); - - return isToday ? 'Today' - : isYesterday ? 'Yesterday' - : isThisWeek ? "Previous 7 days" - : isThisYear ? itemDate.toLocaleDateString('en-US', { month: 'long' }) // month name - : itemDate.toLocaleDateString('en-US', { year: 'numeric' }); // the year -} - -function threadItemsGroupByDate(threadItems) { - const groupedItems = new Map(); - - threadItems.forEach(item => { - const group = threadItemsGetGroupName(item.created); - if (!groupedItems.has(group)) { - groupedItems.set(group, []); - } - groupedItems.get(group).push(item); - }); - - return groupedItems; -} - -async function threadItemsSetTitleIfUntitled(items, userInput, computerResponse) { - if (threadItemIsUntitled(items[0])) { - await threadItemsSetTitle(userInput, computerResponse, items, 0); - } -} - -async function threadItemsSetTitle(userInput, computerResponse, items, i) { - - // What's the system prompt? - const AZURE_OPENAI_SYSTEM_PROMPT = process.env.AZURE_OPENAI_SYSTEM_PROMPT ?? "You are a helpful AI assistant."; - - {{set _IS_OPENAI_ASST_TEMPLATE = false}} - {{@include openai.asst.or.chat.create.openai.node.js}} - - // Prepare the messages for the OpenAI API - let messages = [ - { role: 'system', content: AZURE_OPENAI_SYSTEM_PROMPT }, - { role: 'user', content: userInput }, - { role: 'assistant', content: computerResponse }, - { role: 'system', content: "Please suggest a title for this interaction. Don't be cute or humorous in your answer. Answer only with a factual descriptive title. Do not use quotes. Do not prefix with 'Title:' or anything else. Just emit the title." } - ]; - - // Call the OpenAI API to get a title for the conversation - const completion = await openai.chat.completions.create({ - messages: messages, - {{if {USE_AZURE_OPENAI}}} - model: AZURE_OPENAI_CHAT_DEPLOYMENT - {{else}} - model: OPENAI_MODEL_NAME - {{endif}} - }); - -var newTitle = completion.choices[i].message.content; - items[i].metadata = newTitle; - - localStorage.setItem('threadItems', JSON.stringify(items)); - threadPanelPopulate(items); -} - -function threadPanelPopulate(items) { - - // Clear existing content - const threadPanel = document.getElementById('threadPanel'); - threadPanel.innerHTML = ''; - - // Group thread items by date - const groupedThreadItems = threadItemsGroupByDate(items); - - // Iterate over grouped items and populate thread panel - for (const [date, items] of groupedThreadItems) { - const dateHeader = document.createElement('div'); - dateHeader.classList.add('threadOnDate'); - dateHeader.textContent = date; - threadPanel.appendChild(dateHeader); - - const threadsContainer = document.createElement('div'); - threadsContainer.id = 'threads'; - threadPanel.appendChild(threadsContainer); - - items.forEach(item => { - const button = document.createElement('button'); - button.id = item.id; - button.classList.add('thread', 'w3-button'); - button.onclick = function() { - loadThread(this.id); - }; - - const div = document.createElement('div'); - const icon = document.createElement('i'); - icon.classList.add('threadIcon', 'fa', 'fa-comment'); - - div.appendChild(icon); - div.appendChild(document.createTextNode(item.metadata)); - button.appendChild(div); - threadsContainer.appendChild(button); - }); - } -} - -function userInputTextAreaGetElement() { - return document.getElementById("userInput"); -} - -function userInputTextAreaInit() { - let inputElement = userInputTextAreaGetElement(); - inputElement.addEventListener("keydown", userInputTextAreaHandleKeyDown()); - inputElement.addEventListener("input", userInputTextAreaUpdateHeight); -} - -function userInputTextAreaFocus() { - let inputElement = userInputTextAreaGetElement(); - inputElement.focus(); -} - -function userInputTextAreaClear() { - userInputTextAreaGetElement().value = ''; - userInputTextAreaUpdateHeight(); -} - -function userInputTextAreaUpdateHeight() { - let inputElement = userInputTextAreaGetElement(); - inputElement.style.height = 'auto'; - inputElement.style.height = (userInput.scrollHeight) + 'px'; -} - -function userInputTextAreaHandleKeyDown() { - return function (event) { - if (event.key === "Enter") { - if (!event.shiftKey) { - event.preventDefault(); - sendMessage(); - } - } - }; -} - -function varsInit() { - document.addEventListener('DOMContentLoaded', varsUpdateHeightsAndWidths); - window.addEventListener('resize', varsUpdateHeightsAndWidths); -} - -function varsUpdateHeightsAndWidths() { - let headerHeight = document.querySelector('#header').offsetHeight; - let userInputHeight = document.querySelector('#userInputPanel').offsetHeight; - document.documentElement.style.setProperty('--header-height', headerHeight + 'px'); - document.documentElement.style.setProperty('--input-height', userInputHeight + 'px'); -} - -async function newChat() { - chatPanelClear(); - logoShow(); - userInputTextAreaFocus(); - await assistantCreateOrRetrieveThread(); -} - -async function loadThread(threadId) { - chatPanelClear(); - await assistantCreateOrRetrieveThread(threadId); - userInputTextAreaFocus(); -} - -function sendMessage() { - let inputElement = userInputTextAreaGetElement(); - let inputValue = inputElement.value; - - let notEmpty = inputValue.trim() !== ''; - if (notEmpty) { - let html = markdownToHtml(inputValue) || inputValue.replace(/\n/g, '
'); - chatPanelAppendMessage('user', html); - userInputTextAreaClear(); - varsUpdateHeightsAndWidths(); - assistantProcessInput(inputValue); - } -} - -async function init() { - - const urlParams = new URLSearchParams(window.location.search); - - themeInit(); - markdownInit(); - userInputTextAreaInit(); - varsInit(); - - let items; - await assistantInit(); - - const fake = urlParams.get('fake') === 'true'; - if (fake) { - items = threadItemsLoadFakeData(); - localStorage.setItem('threadItems', JSON.stringify(items)); - } - - const clear = urlParams.get('clear') === 'true'; - if (clear) { - localStorage.removeItem('threadItems'); - items = []; - } - - items = items || threadItemsGet(); - threadPanelPopulate(items); - - userInputTextAreaFocus(); - - window.newChat = newChat; - window.loadThread = loadThread; - window.sendMessage = sendMessage; - window.toggleTheme = toggleTheme; -} - -init(); \ No newline at end of file diff --git a/src/ai/.x/templates/openai-asst-webpage-with-functions-js/webpack.config.js b/src/ai/.x/templates/openai-asst-webpage-with-functions-js/webpack.config.js deleted file mode 100644 index 0b554afa..00000000 --- a/src/ai/.x/templates/openai-asst-webpage-with-functions-js/webpack.config.js +++ /dev/null @@ -1,14 +0,0 @@ -const path = require('path'); -const Dotenv = require('dotenv-webpack'); - -module.exports = { - mode: 'development', - entry: './src/script.js', - output: { - filename: 'main.js', - path: path.resolve(__dirname, 'dist'), - }, - plugins: [ - new Dotenv(), - ], -}; \ No newline at end of file diff --git a/src/ai/.x/templates/openai-asst-webpage-with-functions-js/README.md b/src/ai/.x/templates/openai-asst-webpage-with-functions-ts/README.md similarity index 71% rename from src/ai/.x/templates/openai-asst-webpage-with-functions-js/README.md rename to src/ai/.x/templates/openai-asst-webpage-with-functions-ts/README.md index 08c49f9d..f4dd2775 100644 --- a/src/ai/.x/templates/openai-asst-webpage-with-functions-js/README.md +++ b/src/ai/.x/templates/openai-asst-webpage-with-functions-ts/README.md @@ -1,6 +1,6 @@ -# `ai` chat website +# `ai` chat website (TypeScript Version) -This is a simple website chat interface that uses OpenAI's API to generate text responses to user input. +This is a simple website chat interface that uses OpenAI's API to generate text responses to user input, now fully converted to TypeScript. User input is typed into a text box and added to the conversation as a message inside a chat panel. The panel scrolls up and the computer responds with streaming text output into another message in the chat panel. There is a left nav that has a "new chat" button and has a spot for future expansion w/ a list of historical chats. @@ -10,7 +10,7 @@ To build the website, run the following commands: ```bash npm install -npm run webpack +npm run vite ``` To run the website, launch `index.html` in your browser. @@ -21,12 +21,12 @@ These setup steps are also represented in tasks.json and launch.json, so that yo | Category | File | Description | --- | --- | --- -| **SOURCE CODE** | ai.png | Logo/icon for the website. +| **SOURCE CODE** | favicon.png | Logo/icon for the website. | | index.html | HTML file with controls and layout. | | style.css | CSS file with layout and styling. -| | src/script.js | Main JS file with HTML to JS interactions. -| | src/OpenAIAssistantsFunctionsStreamingClass.js | Main JS file with JS to OpenAI interactions. +| | src/index.ts | Main TS file with HTML to TS interactions. +| | src/OpenAIAssistantsFunctionsStreamingClass.ts | Main TS file with TS to OpenAI interactions. | | | | **BUILD + PACKAGING** | .env | Contains the API keys, endpoints, etc. | | package.json | Contains the dependencies. -| | webpack.config.js | The webpack config file. \ No newline at end of file +| | vite.config.ts | The Vite config file. diff --git a/src/ai/.x/templates/openai-asst-webpage-with-functions-js/_.json b/src/ai/.x/templates/openai-asst-webpage-with-functions-ts/_.json similarity index 90% rename from src/ai/.x/templates/openai-asst-webpage-with-functions-js/_.json rename to src/ai/.x/templates/openai-asst-webpage-with-functions-ts/_.json index 1993e968..e924aced 100644 --- a/src/ai/.x/templates/openai-asst-webpage-with-functions-js/_.json +++ b/src/ai/.x/templates/openai-asst-webpage-with-functions-ts/_.json @@ -1,7 +1,7 @@ { "_LongName": "OpenAI Assistants Webpage (w/ Functions)", "_ShortName": "openai-asst-webpage-with-functions", - "_Language": "JavaScript", + "_Language": "TypeScript", "ClassName": "OpenAIAssistantsFunctionsStreamingClass", "AZURE_OPENAI_AUTH_METHOD": "KEY", @@ -9,4 +9,4 @@ "_IS_BROWSER_TEMPLATE": "true", "_IS_OPENAI_ASST_TEMPLATE": "true" -} \ No newline at end of file +} diff --git a/src/ai/.x/templates/openai-asst-webpage-with-functions-js/index.html b/src/ai/.x/templates/openai-asst-webpage-with-functions-ts/index.html similarity index 84% rename from src/ai/.x/templates/openai-asst-webpage-with-functions-js/index.html rename to src/ai/.x/templates/openai-asst-webpage-with-functions-ts/index.html index 994b6cbd..a1b1250d 100644 --- a/src/ai/.x/templates/openai-asst-webpage-with-functions-js/index.html +++ b/src/ai/.x/templates/openai-asst-webpage-with-functions-ts/index.html @@ -2,10 +2,12 @@ + + Chat Interface @@ -16,8 +18,8 @@
- @@ -41,7 +43,7 @@

chat.contoso.com

- +
@@ -53,7 +55,7 @@

chat.contoso.com

-
@@ -63,8 +65,7 @@

chat.contoso.com

- - + - \ No newline at end of file + diff --git a/src/ai/.x/templates/openai-asst-webpage-with-functions-js/package.json b/src/ai/.x/templates/openai-asst-webpage-with-functions-ts/package.json similarity index 72% rename from src/ai/.x/templates/openai-asst-webpage-with-functions-js/package.json rename to src/ai/.x/templates/openai-asst-webpage-with-functions-ts/package.json index ba672518..678fbfb1 100644 --- a/src/ai/.x/templates/openai-asst-webpage-with-functions-js/package.json +++ b/src/ai/.x/templates/openai-asst-webpage-with-functions-ts/package.json @@ -3,22 +3,23 @@ "version": "1.0.0", "description": "Chat Interface with OpenAI", "main": "script.js", + "type": "module", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "webpack": "webpack" + "start": "vite", + "build": "vite build", + "preview": "vite preview" }, "author": "", "license": "MIT", "dependencies": { - "@azure/identity": "4.1.0", "highlight.js": "^11.7.2", "marked": "^4.0.10", "openai": "^4.31.0" }, "keywords": [], "devDependencies": { - "dotenv-webpack": "^7.0.3", - "webpack": "^5.89.0", - "webpack-cli": "^5.1.4" + "@types/marked": "^6.0.0", + "vite": "^5.2.8" } } diff --git a/src/ai/.x/templates/openai-asst-webpage-with-functions-js/src/FunctionFactory.js b/src/ai/.x/templates/openai-asst-webpage-with-functions-ts/src/FunctionFactory.ts similarity index 71% rename from src/ai/.x/templates/openai-asst-webpage-with-functions-js/src/FunctionFactory.js rename to src/ai/.x/templates/openai-asst-webpage-with-functions-ts/src/FunctionFactory.ts index a7bdab04..2057d5bb 100644 --- a/src/ai/.x/templates/openai-asst-webpage-with-functions-js/src/FunctionFactory.js +++ b/src/ai/.x/templates/openai-asst-webpage-with-functions-ts/src/FunctionFactory.ts @@ -1,17 +1,19 @@ class FunctionFactory { + functions: { [name: string]: { schema: any; function: Function } }; + constructor() { this.functions = {}; } - addFunction(schema, fun) { + addFunction(schema: any, fun: Function): void { this.functions[schema.name] = { schema: schema, function: fun }; } - getFunctionSchemas() { + getFunctionSchemas(): any[] { return Object.values(this.functions).map(value => value.schema); } - getTools() { + getTools(): any[] { return Object.values(this.functions).map(value => { return { type: "function", @@ -20,7 +22,7 @@ class FunctionFactory { }); } - tryCallFunction(function_name, function_arguments) { + tryCallFunction(function_name: string, function_arguments: any): any { const function_info = this.functions[function_name]; if (function_info === undefined) { return undefined; @@ -33,4 +35,4 @@ class FunctionFactory { } } -exports.FunctionFactory = FunctionFactory; \ No newline at end of file +export { FunctionFactory }; diff --git a/src/ai/.x/templates/openai-asst-webpage-with-functions-js/src/OpenAIAssistantsCustomFunctions.js b/src/ai/.x/templates/openai-asst-webpage-with-functions-ts/src/OpenAIAssistantsCustomFunctions.ts similarity index 86% rename from src/ai/.x/templates/openai-asst-webpage-with-functions-js/src/OpenAIAssistantsCustomFunctions.js rename to src/ai/.x/templates/openai-asst-webpage-with-functions-ts/src/OpenAIAssistantsCustomFunctions.ts index 15ed3234..a9c86fc1 100644 --- a/src/ai/.x/templates/openai-asst-webpage-with-functions-js/src/OpenAIAssistantsCustomFunctions.js +++ b/src/ai/.x/templates/openai-asst-webpage-with-functions-ts/src/OpenAIAssistantsCustomFunctions.ts @@ -1,7 +1,7 @@ -const { FunctionFactory } = require("./FunctionFactory"); +import { FunctionFactory } from "./FunctionFactory"; let factory = new FunctionFactory(); -function getCurrentWeather(function_arguments) { +function getCurrentWeather(function_arguments: string): string { const location = JSON.parse(function_arguments).location; return `The weather in ${location} is 72 degrees and sunny.`; }; @@ -27,7 +27,7 @@ const getCurrentWeatherSchema = { factory.addFunction(getCurrentWeatherSchema, getCurrentWeather); -function getCurrentDate() { +function getCurrentDate(): string { const date = new Date(); return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`; } @@ -43,7 +43,7 @@ const getCurrentDateSchema = { factory.addFunction(getCurrentDateSchema, getCurrentDate); -function getCurrentTime() { +function getCurrentTime(): string { const date = new Date(); return `${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`; } @@ -59,4 +59,4 @@ const getCurrentTimeSchema = { factory.addFunction(getCurrentTimeSchema, getCurrentTime); -exports.factory = factory; \ No newline at end of file +export { factory }; diff --git a/src/ai/.x/templates/openai-asst-webpage-with-functions-js/src/OpenAIAssistantsFunctionsStreamingClass.js b/src/ai/.x/templates/openai-asst-webpage-with-functions-ts/src/OpenAIAssistantsFunctionsStreamingClass.ts similarity index 76% rename from src/ai/.x/templates/openai-asst-webpage-with-functions-js/src/OpenAIAssistantsFunctionsStreamingClass.js rename to src/ai/.x/templates/openai-asst-webpage-with-functions-ts/src/OpenAIAssistantsFunctionsStreamingClass.ts index 48b84efb..1a4f0d85 100644 --- a/src/ai/.x/templates/openai-asst-webpage-with-functions-js/src/OpenAIAssistantsFunctionsStreamingClass.js +++ b/src/ai/.x/templates/openai-asst-webpage-with-functions-ts/src/OpenAIAssistantsFunctionsStreamingClass.ts @@ -1,9 +1,9 @@ -const { OpenAI } = require('openai'); +import { OpenAI } from 'openai'; -class {ClassName} { +class OpenAIAssistantsFunctionsStreamingClass { // Constructor - constructor(openAIAssistantId, functionFactory, openai, simulateTypingDelay = 0) { + constructor(private openAIAssistantId: string, private functionFactory: any, private openai: OpenAI, private simulateTypingDelay: number = 0) { this.simulateTypingDelay = simulateTypingDelay; this.openAIAssistantId = openAIAssistantId; this.functionFactory = functionFactory; @@ -19,14 +19,14 @@ class {ClassName} { } // Retrieve an existing thread - async retrieveThread(threadId) { + async retrieveThread(threadId: string) { this.thread =await this.openai.beta.threads.retrieve(threadId); console.log(`Thread ID: ${this.thread.id}`); return this.thread; } // Get the messages in the thread - async getThreadMessages(callback) { + async getThreadMessages(callback: (role: string, content: string) => void) { const messages = await this.openai.beta.threads.messages.list(this.thread.id); messages.data.reverse(); @@ -38,7 +38,7 @@ class {ClassName} { } // Get the response from the Assistant - async getResponse(userInput, callback) { + async getResponse(userInput: string, callback: (content: string) => void) { if (this.thread == null) { await this.createThread(); @@ -61,9 +61,9 @@ class {ClassName} { } // Handle the stream events - async handleStreamEvents(stream, callback) { - stream.on('textDelta', async (textDelta, snapshot) => await this.onTextDelta(textDelta, callback)); - stream.on('event', async (event) => { + async handleStreamEvents(stream: any, callback: (content: string) => void) { + stream.on('textDelta', async (textDelta: any, snapshot: any) => await this.onTextDelta(textDelta, callback)); + stream.on('event', async (event: any) => { if (event.event == 'thread.run.completed') { this.resolveRunCompletedPromise(); } @@ -77,7 +77,7 @@ class {ClassName} { }); } - async onTextDelta(textDelta, callback) { + async onTextDelta(textDelta: any, callback: (content: string) => void) { let content = textDelta.value; if (content != null) { if(callback != null) { @@ -89,7 +89,7 @@ class {ClassName} { } } - async onThreadRunRequiresAction(event, callback) { + async onThreadRunRequiresAction(event: any, callback: (content: string) => void) { let toolCalls = event.data?.required_action?.submit_tool_outputs?.tool_calls; if (toolCalls != null) { let tool_outputs = this.getToolOutputs(toolCalls); @@ -98,7 +98,7 @@ class {ClassName} { } } - getToolOutputs(toolCalls) { + getToolOutputs(toolCalls: any[]) { let tool_outputs = []; for (let toolCall of toolCalls) { if (toolCall.type == 'function') { @@ -113,4 +113,4 @@ class {ClassName} { } } -exports.{ClassName} = {ClassName}; \ No newline at end of file +export { OpenAIAssistantsFunctionsStreamingClass }; diff --git a/src/ai/.x/templates/openai-asst-webpage-with-functions-ts/src/index.ts b/src/ai/.x/templates/openai-asst-webpage-with-functions-ts/src/index.ts new file mode 100644 index 00000000..637b99cf --- /dev/null +++ b/src/ai/.x/templates/openai-asst-webpage-with-functions-ts/src/index.ts @@ -0,0 +1,506 @@ +import { marked } from "marked"; +import hljs from "highlight.js"; +import { OpenAI } from 'openai'; + +import { factory } from "./OpenAIAssistantsCustomFunctions"; +import { {ClassName} } from "./OpenAIAssistantsFunctionsStreamingClass"; + +let assistant: {ClassName}; +async function assistantInit(threadId: string | null = null): Promise { + + // Which assistant, which thread? + const ASSISTANT_ID: string = process.env.ASSISTANT_ID ?? ""; + + {{@include openai.asst.or.chat.create.openai.node.js}} + + // Create the assistants streaming helper class instance + assistant = new {ClassName}(ASSISTANT_ID, factory, openai); + + await assistantCreateOrRetrieveThread(threadId); +} + +async function assistantProcessInput(userInput: string): Promise { + const blackVerticalRectangle: string = '\u25AE'; // Black vertical rectangle ('▮') to simulate an insertion point + + let newMessage: HTMLElement = chatPanelAppendMessage('computer', blackVerticalRectangle); + let completeResponse: string = ""; + + await assistant.getResponse(userInput, function (response: string): void { + let atBottomBeforeUpdate: boolean = chatPanelIsScrollAtBottom(); + + completeResponse += response; + let withEnding: string = `${completeResponse}${blackVerticalRectangle}`; + let asHtml: string | undefined = markdownToHtml(withEnding); + + if (asHtml !== undefined) { + newMessage.innerHTML = asHtml; + + if (atBottomBeforeUpdate) { + chatPanelScrollToBottom(); + } + } + }); + + newMessage.innerHTML = markdownToHtml(completeResponse) || completeResponse.replace(/\n/g, '
'); + chatPanel.scrollTop = chatPanel.scrollHeight; + + await threadItemsCheckIfUpdatesNeeded(userInput, completeResponse); +} + +async function assistantCreateOrRetrieveThread(threadId: string | null = null): Promise { + + if (threadId === null) { + await assistant.createThread() + } else { + await assistant.retrieveThread(threadId); + await assistant.getThreadMessages((role: string, content: string): void => { + let html: string | undefined = markdownToHtml(content) || content.replace(/\n/g, '
'); + role = role === 'user' ? 'user' : 'computer'; + console.log(`role: ${role}, content: ${content}`); + chatPanelAppendMessage(role, html); + }); + } +} + +function chatPanelGetElement(): HTMLElement | null { + return document.getElementById("chatPanel"); +} + +function chatPanelAppendMessage(sender: string, message: string): HTMLElement { + logoHide(); + + let messageContent: HTMLElement = document.createElement("p"); + messageContent.className = "message-content"; + messageContent.innerHTML = message; + + let messageAuthor: HTMLElement = document.createElement("p"); + messageAuthor.className = "message-author"; + messageAuthor.innerHTML = sender == "user" ? "You" : "Assistant"; + + let divContainingBoth: HTMLElement = document.createElement("div"); + divContainingBoth.className = sender === "user" ? "user" : "computer"; + divContainingBoth.appendChild(messageAuthor); + divContainingBoth.appendChild(messageContent); + + let chatPanel: HTMLElement | null = chatPanelGetElement(); + chatPanel.appendChild(divContainingBoth); + chatPanelScrollToBottom(); + + return messageContent; +} + +function chatPanelIsScrollAtBottom(): boolean { + let chatPanel: HTMLElement | null = chatPanelGetElement(); + let atBottom: boolean = Math.abs(chatPanel.scrollHeight - chatPanel.clientHeight - chatPanel.scrollTop) < 1; + return atBottom; +} + +function chatPanelScrollToBottom(): void { + let chatPanel: HTMLElement | null = chatPanelGetElement(); + chatPanel.scrollTop = chatPanel.scrollHeight; +} + +function chatPanelClear(): void { + let chatPanel: HTMLElement | null = chatPanelGetElement(); + chatPanel.innerHTML = ''; +} + +function logoGetElement(): HTMLElement | null { + return document.getElementById("logo"); +} + +function logoShow(): void { + let logo: HTMLElement | null = logoGetElement(); + logo.style.display = "block"; +} + +function logoHide(): void { + let logo: HTMLElement | null = logoGetElement(); + logo.style.display = "none"; +} + +function markdownInit(): void { + marked.setOptions({ + highlight: function (code: string, lang: string): string { + let hl: string = lang === undefined || lang === '' + ? hljs.highlightAuto(code).value + : hljs.highlight(lang, code).value; + return `
${hl}
`; + } + }); +} + +function markdownToHtml(markdownText: string): string | undefined { + try { + return marked.parse(markdownText); + } + catch (error) { + return undefined; + } +} + +function themeInit(): void { + let currentTheme: string | null = localStorage.getItem('theme'); + if (currentTheme === 'dark') { + themeSetDark(); + } + else if (currentTheme === 'light') { + themeSetLight(); + } + toggleThemeButtonInit(); +} + +function themeIsLight(): boolean { + return document.body.classList.contains("light-theme"); +} + +function themeIsDark(): boolean { + return !themeIsLight(); +} + +function toggleTheme(): void { + if (themeIsLight()) { + themeSetDark(); + } else { + themeSetLight(); + } +} + +function themeSetLight(): void { + if (!themeIsLight()) { + document.body.classList.add("light-theme"); + localStorage.setItem('theme', 'light'); + + let iconElement: Element = toggleThemeButtonGetElement().children[0]; + iconElement.classList.remove("fa-toggle-on"); + iconElement.classList.add("fa-toggle-off"); + } +} + +function themeSetDark(): void { + if (!themeIsDark()) { + document.body.classList.remove("light-theme"); + localStorage.setItem('theme', 'dark'); + + let iconElement: Element = toggleThemeButtonGetElement().children[0]; + iconElement.classList.remove("fa-toggle-off"); + iconElement.classList.add("fa-toggle-on"); + } +} + +function toggleThemeButtonGetElement(): HTMLElement { + return document.getElementById("toggleThemeButton"); +} + +function toggleThemeButtonInit(): void { + let buttonElement: HTMLElement = toggleThemeButtonGetElement(); + buttonElement.addEventListener("click", toggleTheme); + buttonElement.addEventListener('keydown', toggleThemeButtonHandleKeyDown()); +} + +function toggleThemeButtonHandleKeyDown(): (event: KeyboardEvent) => void { + return function (event: KeyboardEvent): void { + if (event.code === 'Enter' || event.code === 'Space') { + toggleTheme(); + } + }; +} + +const titleUntitled: string = 'Untitled'; + +interface ThreadItem { + id: string; + created: number; + metadata: string; +} + +function threadItemIsUntitled(item: ThreadItem): boolean { + return item.metadata === titleUntitled; +} + +async function threadItemsCheckIfUpdatesNeeded(userInput: string, computerResponse: string): Promise { + let items: ThreadItem[] = threadItemsGet(); + threadItemsCheckMoveOrAdd(items); + + await threadItemsSetTitleIfUntitled(items, userInput, computerResponse); +} + +function threadItemsCheckMoveOrAdd(items: ThreadItem[]): void { + threadItemsCheckMoveTop(items, assistant.thread.id); + threadItemsCheckAddNew(items, assistant.thread.id); +} + +function threadItemsCheckMoveTop(items: ThreadItem[], threadId: string): void { + let item: ThreadItem | undefined = items.find(item => item.id === threadId); + if (item) { + threadItemsMoveTop(items, item); + } +} + +function threadItemsMoveTop(items: ThreadItem[], item: ThreadItem): void { + var index: number = items.indexOf(item); + if (index !== -1) { + items.splice(index, 1); + } + item.created = Math.floor(Date.now() / 1000); + items.unshift(item); + localStorage.setItem('threadItems', JSON.stringify(items)); + threadPanelPopulate(items); +} + +function threadItemsCheckAddNew(items: ThreadItem[], threadId: string): void { + if (items.length === 0 || items[0].id !== threadId) { + threadItemsAddNew(items, { id: threadId, created: Math.floor(Date.now() / 1000), metadata: titleUntitled }); + } +} + +function threadItemsAddNew(items: ThreadItem[], newItem: ThreadItem): void { + items.unshift(newItem); + localStorage.setItem('threadItems', JSON.stringify(items)); + threadPanelPopulate(items); +} + +function threadItemsGet(): ThreadItem[] { + const threadItemsString: string | null = localStorage.getItem('threadItems'); + if (threadItemsString) { + return JSON.parse(threadItemsString); + } else { + return []; + } +} + +function threadItemsLoadFakeData(): ThreadItem[] { + const now: Date = new Date(); + const yesterday: Date = new Date(new Date().setDate(now.getDate() - 1)); + const thirtyDaysAgo: Date = new Date(new Date().setDate(now.getDate() - 30)); + + const fakeThreadItems: ThreadItem[] = [ + { id: 'thread_XTqDWuGXPjsddI1xctQ2ZD4B', created: Math.floor(now.getTime() / 1000), metadata: 'Skeleton joke' }, + { id: 'thread_wzmGKFC22PKKcvoDs2zrYLD7', created: Math.floor(yesterday.getTime() / 1000), metadata: 'Why is the sky blue?' }, + { id: 'thread_IAxIrq4YJmFflA1fraw7iEcI', created: Math.floor(yesterday.getTime() / 1000), metadata: 'Hello world in C#' }, + { id: 'thread_RAgQWZFf3B3MWjVIpSO6JiRi', created: Math.floor(thirtyDaysAgo.getTime() / 1000), metadata: 'Thread stuff' }, + ]; + return fakeThreadItems; +} + +function threadItemsGetGroupName(timestamp: number): string { + const now: Date = new Date(); + const itemDate: Date = new Date(timestamp * 1000); + const isToday: boolean = itemDate.toDateString() === now.toDateString(); + const isYesterday: boolean = itemDate.toDateString() === new Date(new Date().setDate(now.getDate() - 1)).toDateString(); + const isThisWeek: boolean = itemDate > new Date(new Date().setDate(now.getDate() - 7)); + const isThisYear: boolean = itemDate.getFullYear() === now.getFullYear(); + + return isToday ? 'Today' + : isYesterday ? 'Yesterday' + : isThisWeek ? "Previous 7 days" + : isThisYear ? itemDate.toLocaleDateString('en-US', { month: 'long' }) // month name + : itemDate.toLocaleDateString('en-US', { year: 'numeric' }); // the year +} + +function threadItemsGroupByDate(threadItems: ThreadItem[]): Map { + const groupedItems: Map = new Map(); + + threadItems.forEach(item => { + const group: string = threadItemsGetGroupName(item.created); + if (!groupedItems.has(group)) { + groupedItems.set(group, []); + } + groupedItems.get(group).push(item); + }); + + return groupedItems; +} + +async function threadItemsSetTitleIfUntitled(items: ThreadItem[], userInput: string, computerResponse: string): Promise { + if (threadItemIsUntitled(items[0])) { + await threadItemsSetTitle(userInput, computerResponse, items, 0); + } +} + +async function threadItemsSetTitle(userInput: string, computerResponse: string, items: ThreadItem[], i: number): Promise { + + // What's the system prompt? + const AZURE_OPENAI_SYSTEM_PROMPT: string = process.env.AZURE_OPENAI_SYSTEM_PROMPT ?? "You are a helpful AI assistant."; + + {{set _IS_OPENAI_ASST_TEMPLATE = false}} + {{@include openai.asst.or.chat.create.openai.node.js}} + + // Prepare the messages for the OpenAI API + let messages: any[] = [ + { role: 'system', content: AZURE_OPENAI_SYSTEM_PROMPT }, + { role: 'user', content: userInput }, + { role: 'assistant', content: computerResponse }, + { role: 'system', content: "Please suggest a title for this interaction. Don't be cute or humorous in your answer. Answer only with a factual descriptive title. Do not use quotes. Do not prefix with 'Title:' or anything else. Just emit the title." } + ]; + + // Call the OpenAI API to get a title for the conversation + const completion = await openai.chat.completions.create({ + messages: messages, + {{if {USE_AZURE_OPENAI}}} + model: AZURE_OPENAI_CHAT_DEPLOYMENT + {{else}} + model: OPENAI_MODEL_NAME + {{endif}} + }); + +var newTitle: string = completion.choices[i].message.content; + items[i].metadata = newTitle; + + localStorage.setItem('threadItems', JSON.stringify(items)); + threadPanelPopulate(items); +} + +function threadPanelPopulate(items: ThreadItem[]): void { + + // Clear existing content + const threadPanel: HTMLElement = document.getElementById('threadPanel'); + threadPanel.innerHTML = ''; + + // Group thread items by date + const groupedThreadItems: Map = threadItemsGroupByDate(items); + + // Iterate over grouped items and populate thread panel + for (const [date, items] of groupedThreadItems) { + const dateHeader: HTMLElement = document.createElement('div'); + dateHeader.classList.add('threadOnDate'); + dateHeader.textContent = date; + threadPanel.appendChild(dateHeader); + + const threadsContainer: HTMLElement = document.createElement('div'); + threadsContainer.id = 'threads'; + threadPanel.appendChild(threadsContainer); + + items.forEach(item => { + const button: HTMLElement = document.createElement('button'); + button.id = item.id; + button.classList.add('thread', 'w3-button'); + button.onclick = function() { + loadThread(this.id); + }; + + const div: HTMLElement = document.createElement('div'); + const icon: HTMLElement = document.createElement('i'); + icon.classList.add('threadIcon', 'fa', 'fa-comment'); + + div.appendChild(icon); + div.appendChild(document.createTextNode(item.metadata)); + button.appendChild(div); + threadsContainer.appendChild(button); + }); + } +} + +function userInputTextAreaGetElement(): HTMLTextAreaElement { + return document.getElementById("userInput") as HTMLTextAreaElement; +} + +function userInputTextAreaInit(): void { + let inputElement: HTMLTextAreaElement = userInputTextAreaGetElement(); + inputElement.addEventListener("keydown", userInputTextAreaHandleKeyDown()); + inputElement.addEventListener("input", userInputTextAreaUpdateHeight); +} + +function userInputTextAreaFocus(): void { + let inputElement: HTMLTextAreaElement = userInputTextAreaGetElement(); + inputElement.focus(); +} + +function userInputTextAreaClear(): void { + let inputElement: HTMLTextAreaElement = userInputTextAreaGetElement(); + inputElement.value = ''; + userInputTextAreaUpdateHeight(); +} + +function userInputTextAreaUpdateHeight(): void { + let inputElement: HTMLTextAreaElement = userInputTextAreaGetElement(); + inputElement.style.height = 'auto'; + inputElement.style.height = (inputElement.scrollHeight) + 'px'; +} + +function userInputTextAreaHandleKeyDown(): (event: KeyboardEvent) => void { + return function (event: KeyboardEvent): void { + if (event.key === "Enter") { + if (!event.shiftKey) { + event.preventDefault(); + sendMessage(); + } + } + }; +} + +function varsInit(): void { + document.addEventListener('DOMContentLoaded', varsUpdateHeightsAndWidths); + window.addEventListener('resize', varsUpdateHeightsAndWidths); +} + +function varsUpdateHeightsAndWidths(): void { + let headerHeight: number = document.querySelector('#header').offsetHeight; + let userInputHeight: number = document.querySelector('#userInputPanel').offsetHeight; + document.documentElement.style.setProperty('--header-height', headerHeight + 'px'); + document.documentElement.style.setProperty('--input-height', userInputHeight + 'px'); +} + +async function newChat(): Promise { + chatPanelClear(); + logoShow(); + userInputTextAreaFocus(); + await assistantCreateOrRetrieveThread(); +} + +async function loadThread(threadId: string): Promise { + chatPanelClear(); + await assistantCreateOrRetrieveThread(threadId); + userInputTextAreaFocus(); +} + +function sendMessage(): void { + let inputElement: HTMLTextAreaElement = userInputTextAreaGetElement(); + let inputValue: string = inputElement.value; + + let notEmpty: boolean = inputValue.trim() !== ''; + if (notEmpty) { + let html: string | undefined = markdownToHtml(inputValue) || inputValue.replace(/\n/g, '
'); + chatPanelAppendMessage('user', html); + userInputTextAreaClear(); + varsUpdateHeightsAndWidths(); + assistantProcessInput(inputValue); + } +} + +async function init(): Promise { + + const urlParams: URLSearchParams = new URLSearchParams(window.location.search); + + themeInit(); + markdownInit(); + userInputTextAreaInit(); + varsInit(); + + let items: ThreadItem[]; + await assistantInit(); + + const fake: boolean = urlParams.get('fake') === 'true'; + if (fake) { + items = threadItemsLoadFakeData(); + localStorage.setItem('threadItems', JSON.stringify(items)); + } + + const clear: boolean = urlParams.get('clear') === 'true'; + if (clear) { + localStorage.removeItem('threadItems'); + items = []; + } + + items = items || threadItemsGet(); + threadPanelPopulate(items); + + userInputTextAreaFocus(); + + window.newChat = newChat; + window.loadThread = loadThread; + window.sendMessage = sendMessage; + window.toggleTheme = toggleTheme; +} + +init(); diff --git a/src/ai/.x/templates/openai-asst-webpage-with-functions-ts/src/vite-env.d.ts b/src/ai/.x/templates/openai-asst-webpage-with-functions-ts/src/vite-env.d.ts new file mode 100644 index 00000000..baa32c34 --- /dev/null +++ b/src/ai/.x/templates/openai-asst-webpage-with-functions-ts/src/vite-env.d.ts @@ -0,0 +1,16 @@ +/// + +interface ImportMetaEnv { + readonly ASSISTANT_ID: string; + readonly AZURE_OPENAI_API_KEY: string; + readonly AZURE_OPENAI_API_VERSION: string; + readonly AZURE_OPENAI_ENDPOINT: string; + readonly AZURE_OPENAI_CHAT_DEPLOYMENT: string; + readonly OPENAI_API_KEY: string; + readonly OPENAI_ORG_ID: string; + readonly OPENAI_MODEL_NAME: string; + } + + interface ImportMeta { + readonly env: ImportMetaEnv + } diff --git a/src/ai/.x/templates/openai-asst-webpage-with-functions-js/style.css b/src/ai/.x/templates/openai-asst-webpage-with-functions-ts/style.css similarity index 100% rename from src/ai/.x/templates/openai-asst-webpage-with-functions-js/style.css rename to src/ai/.x/templates/openai-asst-webpage-with-functions-ts/style.css diff --git a/src/ai/.x/templates/openai-asst-webpage-with-functions-ts/tsconfig.json b/src/ai/.x/templates/openai-asst-webpage-with-functions-ts/tsconfig.json new file mode 100644 index 00000000..f7d0926b --- /dev/null +++ b/src/ai/.x/templates/openai-asst-webpage-with-functions-ts/tsconfig.json @@ -0,0 +1,38 @@ +{ + "compilerOptions": { + "target": "es5", + "module": "esnext", + "strict": true, + "jsx": "preserve", + "importHelpers": true, + "moduleResolution": "node", + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "sourceMap": true, + "baseUrl": ".", + "types": [ + "vite/client" + ], + "paths": { + "@/*": [ + "src/*" + ] + }, + "lib": [ + "esnext", + "dom", + "dom.iterable", + "scripthost" + ] + }, + "include": [ + "src/**/*.ts", + "src/**/*.d.ts", + "src/**/*.tsx", + "src/vite-env.d.ts" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/src/ai/.x/templates/openai-asst-webpage-with-functions-ts/vite.config.ts b/src/ai/.x/templates/openai-asst-webpage-with-functions-ts/vite.config.ts new file mode 100644 index 00000000..6446c690 --- /dev/null +++ b/src/ai/.x/templates/openai-asst-webpage-with-functions-ts/vite.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [], + server: { + port: 3000 + }, + build: { + outDir: 'dist', + rollupOptions: { + input: './src/index.ts' + } + } +});