mirror of
https://github.com/deiucanta/chatpad.git
synced 2026-03-11 09:04:31 +00:00
feat: app actions
This commit is contained in:
parent
b0a72d8517
commit
09c01e4b38
16 changed files with 3438 additions and 106 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -2,3 +2,4 @@ node_modules
|
|||
.parcel-cache
|
||||
dist
|
||||
notes.txt
|
||||
lib
|
||||
|
|
@ -21,3 +21,4 @@ micros:
|
|||
dev: npm run dev
|
||||
public_routes:
|
||||
- "/public/*"
|
||||
provide_actions: true
|
||||
|
|
|
|||
|
|
@ -1,38 +0,0 @@
|
|||
import express from 'express'
|
||||
import { Deta } from 'deta'
|
||||
|
||||
const app = express()
|
||||
const deta = Deta()
|
||||
|
||||
const chats = deta.Base('chats')
|
||||
const messages = deta.Base('messages')
|
||||
|
||||
app.use(express.json())
|
||||
|
||||
app.get('/public/chats/:key', async (req, res) => {
|
||||
const key = req.params.key
|
||||
|
||||
const chat = await chats.get(key)
|
||||
if (!chat || !chat.shared) {
|
||||
return res.status(404).json({ message: 'Chat not found' })
|
||||
}
|
||||
|
||||
res.json(chat)
|
||||
})
|
||||
|
||||
app.get('/public/chats/:key/messages', async (req, res) => {
|
||||
const key = req.params.key
|
||||
|
||||
const chat = await chats.get(key)
|
||||
if (!chat || !chat.shared) {
|
||||
return res.status(404).json({ message: 'Chat not found' })
|
||||
}
|
||||
|
||||
const items = await messages.fetch({ chatId: key }, { desc: false })
|
||||
res.json(items)
|
||||
})
|
||||
|
||||
const PORT = process.env.PORT || 3000
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server is running on port ${PORT}`)
|
||||
})
|
||||
3005
backend/package-lock.json
generated
3005
backend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -2,17 +2,28 @@
|
|||
"name": "backend",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"main": "src/index.ts",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "node --watch index.js",
|
||||
"start": "node index.js"
|
||||
"build": "tsc",
|
||||
"dev": "nodemon --esm src/index.ts",
|
||||
"start": "ts-node --esm src/index.ts"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"deta": "^2.0.0",
|
||||
"express": "^4.18.2"
|
||||
"deta-space-actions": "^0.1.9",
|
||||
"express": "^4.18.2",
|
||||
"nanoid": "^3.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/node": "^20.6.3",
|
||||
"nodemon": "^3.0.1",
|
||||
"ts-loader": "^9.4.4",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^5.2.2"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
18
backend/src/db.ts
Normal file
18
backend/src/db.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { Deta } from 'deta'
|
||||
import { nanoid } from "nanoid"
|
||||
|
||||
export * from '../../src/db/types.js'
|
||||
|
||||
const deta = Deta()
|
||||
|
||||
export const chats = deta.Base('chats')
|
||||
export const prompts = deta.Base('prompts')
|
||||
export const messages = deta.Base('messages')
|
||||
export const settings = deta.Base('settings')
|
||||
|
||||
export const generateKey = (ascending = true) => {
|
||||
const maxDateNowValue = 8.64e15
|
||||
const timestamp = ascending ? Date.now() : maxDateNowValue - Date.now()
|
||||
|
||||
return `${ timestamp.toString(16) }${ nanoid(5) }`
|
||||
}
|
||||
205
backend/src/index.ts
Normal file
205
backend/src/index.ts
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
import express from 'express'
|
||||
import { CardType, createActions, Inputs } from 'deta-space-actions'
|
||||
|
||||
import { chats, messages, prompts, settings, generateKey } from './db.js'
|
||||
import type { Chat, Prompt, Settings } from './db.js'
|
||||
import { createChatWithMessage } from './openai.js'
|
||||
|
||||
const app = express()
|
||||
const actions = createActions()
|
||||
|
||||
const domain = process.env.DETA_SPACE_APP_HOSTNAME
|
||||
|
||||
app.use(express.json())
|
||||
|
||||
app.get('/public/chats/:key', async (req, res) => {
|
||||
const key = req.params.key
|
||||
|
||||
const chat = await chats.get(key)
|
||||
if (!chat || !chat.shared) {
|
||||
return res.status(404).json({ message: 'Chat not found' })
|
||||
}
|
||||
|
||||
res.json(chat)
|
||||
})
|
||||
|
||||
app.get('/public/chats/:key/messages', async (req, res) => {
|
||||
const key = req.params.key
|
||||
|
||||
const chat = await chats.get(key)
|
||||
if (!chat || !chat.shared) {
|
||||
return res.status(404).json({ message: 'Chat not found' })
|
||||
}
|
||||
|
||||
const items = await messages.fetch({ chatId: key }, { desc: false })
|
||||
res.json(items)
|
||||
})
|
||||
|
||||
actions.add<{ prompt?: string }>({
|
||||
name: 'create_chat',
|
||||
title: 'Create Chat',
|
||||
input: [
|
||||
Inputs('prompt').String().Optional()
|
||||
],
|
||||
card: CardType.DETAIL,
|
||||
handler: async (event) => {
|
||||
console.log(`Creating new chat`)
|
||||
|
||||
const { prompt: promptName } = event
|
||||
|
||||
let prompt
|
||||
if (promptName) {
|
||||
const { items } = await prompts.fetch({ title: promptName })
|
||||
prompt = items[0]
|
||||
if (!prompt) {
|
||||
return {
|
||||
title: 'Prompt not found',
|
||||
description: 'Please try again with a valid prompt key',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const chat = await chats.put({
|
||||
description: "New Chat",
|
||||
...(prompt ? {
|
||||
prompt: prompt.key,
|
||||
writingInstructions: prompt.content,
|
||||
writingCharacter: prompt.writingCharacter,
|
||||
writingTone: prompt.writingTone,
|
||||
writingStyle: prompt.writingStyle,
|
||||
writingFormat: prompt.writingFormat,
|
||||
} : {}),
|
||||
totalTokens: 0,
|
||||
private: false,
|
||||
createdAt: new Date().toISOString(),
|
||||
}, generateKey())
|
||||
|
||||
if (!chat) {
|
||||
return {
|
||||
title: 'Error creating chat',
|
||||
description: 'Please try again',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
title: chat?.description,
|
||||
url: `https://${ domain }/chats/${ chat?.key }`
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
actions.add<{ prompt?: string, message: string }>({
|
||||
name: 'run_prompt',
|
||||
title: 'Get Chat Response',
|
||||
input: [
|
||||
Inputs('message').String(),
|
||||
Inputs('prompt').String().Optional()
|
||||
],
|
||||
card: CardType.DETAIL,
|
||||
handler: async (event) => {
|
||||
const { prompt: promptName, message } = event
|
||||
|
||||
let prompt
|
||||
if (promptName) {
|
||||
console.log(`Checking if prompt exists`)
|
||||
const { items } = await prompts.fetch({ title: promptName })
|
||||
prompt = items[0] as unknown as Prompt
|
||||
if (!prompt) {
|
||||
console.log(`Prompt not found`)
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Creating new chat`)
|
||||
const chatRes = await chats.put({
|
||||
description: "New Chat",
|
||||
...(prompt ? {
|
||||
prompt: prompt.key,
|
||||
writingInstructions: prompt.content,
|
||||
writingCharacter: prompt.writingCharacter,
|
||||
writingTone: prompt.writingTone,
|
||||
writingStyle: prompt.writingStyle,
|
||||
writingFormat: prompt.writingFormat,
|
||||
} : {
|
||||
writingInstructions: promptName,
|
||||
}),
|
||||
totalTokens: 0,
|
||||
private: false,
|
||||
createdAt: new Date().toISOString(),
|
||||
}, generateKey())
|
||||
|
||||
if (!chatRes) {
|
||||
return {
|
||||
title: 'Error creating chat',
|
||||
description: 'Please try again',
|
||||
}
|
||||
}
|
||||
|
||||
let chat = (chatRes as unknown as Chat)
|
||||
|
||||
const config = (await settings.get('general')) as unknown as Settings;
|
||||
|
||||
console.log(`Generating chat response`)
|
||||
const response = await createChatWithMessage(chat, config, message, prompt)
|
||||
|
||||
console.log('response:', response)
|
||||
|
||||
chat = (await chats.get(chat?.key)) as unknown as Chat
|
||||
|
||||
return {
|
||||
title: chat?.description,
|
||||
text: response,
|
||||
url: `http://localhost:4200/chats/${ chat?.key }`
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
actions.add({
|
||||
name: 'list_chats',
|
||||
title: 'List Chats',
|
||||
input: [],
|
||||
card: CardType.LIST,
|
||||
handler: async () => {
|
||||
console.log(`Getting all chats`)
|
||||
const { items } = await chats.fetch({})
|
||||
|
||||
return {
|
||||
items: items.map((chat) => ({
|
||||
title: chat.description,
|
||||
url: `https://${ domain }/chats/${ chat.key }`,
|
||||
card: {
|
||||
type: '@deta/detail',
|
||||
data: {
|
||||
title: chat.description,
|
||||
ref: `https://${ domain }/chats/${ chat.key }`,
|
||||
url: `https://${ domain }/chats/${ chat.key }`,
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
actions.add({
|
||||
name: 'list_prompts',
|
||||
title: 'List Prompts',
|
||||
input: [],
|
||||
card: CardType.LIST,
|
||||
handler: async () => {
|
||||
console.log(`Getting all prompts`)
|
||||
const { items } = await prompts.fetch({})
|
||||
|
||||
return {
|
||||
items: items.map((prompt) => ({
|
||||
title: prompt.title,
|
||||
url: `https://${ domain }/chats/${ prompt.key }`,
|
||||
}))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
app.use(actions.middleware)
|
||||
|
||||
const PORT = process.env.PORT || 3000
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server is running on port ${PORT}`)
|
||||
})
|
||||
113
backend/src/openai.ts
Normal file
113
backend/src/openai.ts
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
import { chats, messages, prompts, generateKey, Prompt, Message, Chat, Settings } from './db.js'
|
||||
|
||||
import { getSystemMessage, createChatCompletion } from '../../src/utils/openai.js'
|
||||
|
||||
export const createChatWithMessage = async (chat: Chat, settings: Settings, content: string, prompt?: Prompt, previousMessages?: Message[]) => {
|
||||
let systemMessageValue = ""
|
||||
if (prompt) {
|
||||
systemMessageValue = getSystemMessage({
|
||||
content: prompt.content,
|
||||
character: prompt?.writingCharacter ?? undefined,
|
||||
tone: prompt?.writingTone ?? undefined,
|
||||
style: prompt?.writingStyle ?? undefined,
|
||||
format: prompt?.writingFormat ?? undefined,
|
||||
})
|
||||
|
||||
const updates = {
|
||||
prompt: prompt.key,
|
||||
writingInstructions: prompt.content,
|
||||
writingCharacter: prompt.writingCharacter,
|
||||
writingTone: prompt.writingTone,
|
||||
writingStyle: prompt.writingStyle,
|
||||
writingFormat: prompt.writingFormat,
|
||||
}
|
||||
await chats.update(updates, chat.key)
|
||||
}
|
||||
|
||||
let model = settings?.openAiModel
|
||||
if (chat?.model) {
|
||||
model = chat.model
|
||||
}
|
||||
|
||||
if (!systemMessageValue) {
|
||||
systemMessageValue = getSystemMessage({
|
||||
content: chat?.writingInstructions ?? undefined,
|
||||
character: chat?.writingCharacter ?? undefined,
|
||||
tone: chat?.writingTone ?? undefined,
|
||||
style: chat?.writingStyle ?? undefined,
|
||||
format: chat?.writingFormat ?? undefined,
|
||||
})
|
||||
}
|
||||
|
||||
await messages.put({
|
||||
chatId: chat.key,
|
||||
content,
|
||||
role: "user",
|
||||
createdAt: new Date().toISOString(),
|
||||
}, generateKey())
|
||||
|
||||
const systemMessage = await messages.put({
|
||||
chatId: chat.key,
|
||||
content: "█",
|
||||
role: "assistant",
|
||||
createdAt: new Date().toISOString(),
|
||||
}, generateKey())
|
||||
|
||||
const messageId = systemMessage!.key as string
|
||||
|
||||
const completionResponse = await createChatCompletion(
|
||||
{ ...settings, openAiModel: model },
|
||||
[
|
||||
{
|
||||
role: "system",
|
||||
content: systemMessageValue,
|
||||
},
|
||||
...(previousMessages ?? []).map((message) => ({
|
||||
role: message.role,
|
||||
content: message.content,
|
||||
})),
|
||||
{ role: "user", content },
|
||||
]
|
||||
);
|
||||
|
||||
const completionContent =
|
||||
completionResponse.data.choices[0].message?.content;
|
||||
|
||||
await messages.update({ content: completionContent }, messageId);
|
||||
|
||||
if (chat?.description === "New Chat" || chat?.description === "New Private Chat") {
|
||||
const res = await messages.fetch({ chatId: chat.key })
|
||||
const allMessages = res.items as unknown as Message[]
|
||||
const createChatDescription = await createChatCompletion(
|
||||
{ ...settings, openAiModel: model },
|
||||
[
|
||||
{
|
||||
role: "system",
|
||||
content: systemMessageValue,
|
||||
},
|
||||
...(allMessages ?? []).map((message) => ({
|
||||
role: message.role,
|
||||
content: message.content,
|
||||
})),
|
||||
{
|
||||
role: "user",
|
||||
content:
|
||||
"What would be a short and relevant title for this chat ? You must strictly answer with only the title, no other text is allowed. Don't use quotation marks, just return the text.",
|
||||
},
|
||||
]
|
||||
);
|
||||
const chatDescription =
|
||||
createChatDescription.data.choices[0].message?.content;
|
||||
|
||||
if (createChatDescription.data.usage) {
|
||||
const chatUpdates = {
|
||||
description: chatDescription ?? "New Chat",
|
||||
// todo: add to existing count instead of replacing
|
||||
totalTokens: createChatDescription.data.usage!.total_tokens,
|
||||
}
|
||||
await chats.update(chatUpdates, chat.key)
|
||||
}
|
||||
}
|
||||
|
||||
return completionContent
|
||||
}
|
||||
17
backend/tsconfig.json
Normal file
17
backend/tsconfig.json
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"outDir": "lib",
|
||||
"module": "ES2020",
|
||||
"moduleResolution": "node",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"display": "Recommended",
|
||||
"include": ["src/**/*", "../../src/db", "../../src/utils"],
|
||||
"exclude": ["node_modules", "**/*.spec.ts"]
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
"name": "dialogue",
|
||||
"private": true,
|
||||
"source": "src/main.tsx",
|
||||
"type": "module",
|
||||
"browserslist": "> 0.5%, last 2 versions, not dead",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{
|
||||
export default {
|
||||
"defaultModel": "gpt-3.5-turbo",
|
||||
"defaultType": "openai",
|
||||
"defaultAuth": "api-key",
|
||||
|
|
@ -1,51 +1,7 @@
|
|||
import { Deta } from "deta";
|
||||
import "dexie-export-import";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
export interface Chat {
|
||||
key: string;
|
||||
description: string;
|
||||
totalTokens: number;
|
||||
prompt?: string | null;
|
||||
writingInstructions?: string | null;
|
||||
writingCharacter?: string | null;
|
||||
writingTone?: string | null;
|
||||
writingStyle?: string | null;
|
||||
writingFormat?: string | null;
|
||||
model?: string | null;
|
||||
private?: boolean;
|
||||
shared?: boolean;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface Message {
|
||||
key: string;
|
||||
chatId: string;
|
||||
role: "system" | "assistant" | "user";
|
||||
content: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface Prompt {
|
||||
key: string;
|
||||
title: string;
|
||||
content: string;
|
||||
writingCharacter?: string | null;
|
||||
writingTone?: string | null;
|
||||
writingStyle?: string | null;
|
||||
writingFormat?: string | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface Settings {
|
||||
key: "general";
|
||||
openAiApiKey?: string;
|
||||
openAiModel?: string;
|
||||
openAiApiType?: 'openai' | 'custom';
|
||||
openAiApiAuth?: 'none' | 'bearer-token' | 'api-key';
|
||||
openAiApiBase?: string;
|
||||
openAiApiVersion?: string;
|
||||
}
|
||||
export * from './types.js'
|
||||
|
||||
export const deta = Deta()
|
||||
|
||||
|
|
@ -54,6 +10,7 @@ export const detaDB = {
|
|||
messages: deta.Base("messages"),
|
||||
prompts: deta.Base("prompts"),
|
||||
settings: deta.Base("settings"),
|
||||
integrations: deta.Base("integrations"),
|
||||
}
|
||||
|
||||
// Used as large number to make sure keys are generated in descending order
|
||||
|
|
|
|||
50
src/db/types.ts
Normal file
50
src/db/types.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
export interface Chat {
|
||||
key: string;
|
||||
description: string;
|
||||
totalTokens: number;
|
||||
prompt?: string | null;
|
||||
writingInstructions?: string | null;
|
||||
writingCharacter?: string | null;
|
||||
writingTone?: string | null;
|
||||
writingStyle?: string | null;
|
||||
writingFormat?: string | null;
|
||||
model?: string | null;
|
||||
private?: boolean;
|
||||
shared?: boolean;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface Message {
|
||||
key: string;
|
||||
chatId: string;
|
||||
role: "system" | "assistant" | "user";
|
||||
content: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface Prompt {
|
||||
key: string;
|
||||
title: string;
|
||||
content: string;
|
||||
writingCharacter?: string | null;
|
||||
writingTone?: string | null;
|
||||
writingStyle?: string | null;
|
||||
writingFormat?: string | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface Settings {
|
||||
key: "general";
|
||||
openAiApiKey?: string;
|
||||
openAiModel?: string;
|
||||
openAiApiType?: 'openai' | 'custom';
|
||||
openAiApiAuth?: 'none' | 'bearer-token' | 'api-key';
|
||||
openAiApiBase?: string;
|
||||
openAiApiVersion?: string;
|
||||
}
|
||||
|
||||
export interface Integration {
|
||||
key: string;
|
||||
instance: string
|
||||
apiKey: string
|
||||
}
|
||||
13
src/main.tsx
13
src/main.tsx
|
|
@ -2,12 +2,9 @@ import React from 'react'
|
|||
import ReactDOM from 'react-dom/client'
|
||||
import { App } from './components/App'
|
||||
import './styles/markdown.scss'
|
||||
import { loadConfig } from './utils/config'
|
||||
|
||||
loadConfig().then(() => {
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
)
|
||||
})
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import configData from '../config.js'
|
||||
|
||||
interface Config {
|
||||
defaultModel: AvailableModel["value"];
|
||||
defaultType: 'openai' | 'custom';
|
||||
|
|
@ -35,8 +37,4 @@ interface WritingFormat {
|
|||
label: string;
|
||||
}
|
||||
|
||||
export let config: Config;
|
||||
|
||||
export async function loadConfig() {
|
||||
config = await import('../config.json') as Config;
|
||||
}
|
||||
export let config = configData as Config
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { encode } from "gpt-token-utils";
|
||||
import { ChatCompletionRequestMessage, Configuration, OpenAIApi } from "openai";
|
||||
import { OpenAIExt } from "openai-ext";
|
||||
import { Settings, detaDB } from "../db";
|
||||
import { config } from "./config";
|
||||
import { useDebounce } from "./debounce";
|
||||
import { Settings, detaDB } from "../db/index.js";
|
||||
import { config } from "./config.js";
|
||||
import { useDebounce } from "./debounce.js";
|
||||
|
||||
function getClient(
|
||||
apiKey: string,
|
||||
|
|
|
|||
Loading…
Reference in a new issue