Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .github/ISSUE_TEMPLATE/sweep-template.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
name: Sweep Issue
title: 'Sweep: '
description: For small bugs, features, refactors, and tests to be handled by Sweep, an AI-powered junior developer.
labels: sweep
body:
- type: textarea
id: description
attributes:
label: Details
description: Tell Sweep where and what to edit and provide enough context for a new developer to the codebase
placeholder: |
Unit Tests: Write unit tests for <FILE>. Test each function in the file. Make sure to test edge cases.
Bugs: The bug might be in <FILE>. Here are the logs: ...
Features: the new endpoint should use the ... class from <FILE> because it contains ... logic.
Refactors: We are migrating this function to ... version because ...
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,6 @@ Thank you to all our supporters!🙏
## License

MIT © [Muspi Merol](./LICENSE)
| `SECRET_KEY` | Secret key for the application. | `null` |
| `SITE_PASSWORD` | Password for the site. If not set, the site will be public. | `null` |
| `OPENAI_API_MODEL` | ID of the model to use. | `null` |
22 changes: 18 additions & 4 deletions src/components/Generator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import { useThrottleFn } from 'solidjs-use'
import { generateSignature } from '@/utils/auth'
import { fetchModeration, fetchTitle, fetchTranslation } from '@/utils/misc'
import { audioChunks, getAudioBlob, startRecording, stopRecording } from '@/utils/record'
import { countTokens } from '@/utils/tiktoken'
import { countTokens, tokenCountCache } from '@/utils/tiktoken'
import { MessagesEvent } from '@/utils/events'

import IconClear from './icons/Clear'
import MessageItem from './MessageItem'
import SystemRoleSettings from './SystemRoleSettings'
Expand Down Expand Up @@ -46,6 +47,10 @@ export default () => {
return systemRole
}

const syncMessageList = () => {
localStorage.setItem('messageList', JSON.stringify(messageList()))
}

const setStick = (stick: boolean) => {
_setStick(stick) ? localStorage.setItem('stickToBottom', 'stick') : localStorage.removeItem('stickToBottom')
return stick
Expand All @@ -66,8 +71,11 @@ export default () => {
setMounted(true)

try {
if (localStorage.getItem('messageList'))
if (localStorage.getItem('messageList')) {
setMessageList(JSON.parse(localStorage.getItem('messageList') ?? '[]'))
if (localStorage.getItem('title')) setPageTitle(localStorage.getItem('title')!)
else updatePageTitle(messageList()[0].content)
}

if (localStorage.getItem('stickToBottom') === 'stick')
setStick(true)
Expand Down Expand Up @@ -128,6 +136,7 @@ export default () => {
titleRef && (titleRef.innerHTML = title)
const subTitleRef: HTMLSpanElement | null = document.querySelector('span.gpt-subtitle')
subTitleRef?.classList.toggle('hidden', title !== 'Endless Chat')
title !== 'Free Chat' ? localStorage.setItem('title', title) : localStorage.removeItem('title')
}

const moderationCache: Record<string, string[]> = {}
Expand Down Expand Up @@ -198,6 +207,7 @@ export default () => {

smoothToBottom()
requestWithLatestMessage()
syncMessageList()
}

const toBottom = (behavior: 'smooth' | 'instant') => {
Expand Down Expand Up @@ -343,21 +353,24 @@ export default () => {
})
setStreaming(false)
setController(null)
localStorage.setItem('messageList', JSON.stringify(messageList()))
syncMessageList()
}
}

const clear = () => {
document.dispatchEvent(new MessagesEvent('clearMessages', messageList().length + Number(Boolean(currentSystemRoleSettings()))))
inputRef.value = ''
inputRef.style.height = 'auto'
tokenCountCache.clear()
batch(() => {
setInputValue('')
setMessageList([])
// setCurrentAssistantMessage('')
// setCurrentSystemRoleSettings('')
})
localStorage.setItem('messageList', JSON.stringify([]))

setMessageList([])
syncMessageList()
setCurrentError(null)
setPageTitle()
}
Expand All @@ -376,6 +389,7 @@ export default () => {
setMessageList(messageList().slice(0, -1))

requestWithLatestMessage()
syncMessageList()
}
}

Expand Down
23 changes: 17 additions & 6 deletions src/components/MessageItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,15 @@ export default ({ role, message, showRetry, onRetry }: Props) => {
}
})

function heuristicPatch(markdown: string) {
const pattern = /(^|\n)```\S*$/
const matches = markdown.match(/```/g)

return (matches && matches.length % 2 === 1 && pattern.test(markdown))
? markdown.replace(pattern, '\n```')
: markdown
}

const htmlString = () => {
const md = MarkdownIt({
linkify: true,
Expand All @@ -58,12 +67,14 @@ export default ({ role, message, showRetry, onRetry }: Props) => {
</div>`
}

if (typeof message === 'function')
return md.render(message())
else if (typeof message === 'string')
return md.render(message)

return ''
switch (typeof message) {
case 'function':
return md.render(heuristicPatch(message()))
case 'string':
return md.render(heuristicPatch(message))
default:
return ''
}
}

return (
Expand Down
2 changes: 1 addition & 1 deletion src/utils/misc.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export const fetchTitle = (input: string) => (fetch('/api/title-gen', { method: 'POST', body: input }).then(res => res.text()))
export const fetchTitle = (input: string) => (fetch('/api/title-gen', { method: 'POST', body: input, headers: (localStorage.getItem('apiKey')) ? { authorization: `Bearer ${localStorage.getItem('apiKey')}` } : {} }).then(res => res.text()))

export const fetchTranslation = (input: string) => (fetch(`/api/translate?text=${encodeURIComponent(input)}`).then(res => res.text()))

Expand Down
18 changes: 14 additions & 4 deletions src/utils/tiktoken.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,32 @@
import type { ChatMessage } from '@/types'
import type { Tiktoken } from 'tiktoken'

export const tokenCountCache = new Map<string, number>()

const countTokensSingleMessage = (enc: Tiktoken, message: ChatMessage) => {
return 4 + enc.encode(message.content).length // im_start, im_end, role/name, "\n"
}

export const countTokens = (enc: Tiktoken | null, messages: ChatMessage[]) => {
const countTokensSingleMessageWithCache = (enc: Tiktoken, cacheIt: boolean, message: ChatMessage) => {
if (tokenCountCache.has(message.content)) return tokenCountCache.get(message.content)!

const count = countTokensSingleMessage(enc, message)
if (cacheIt) tokenCountCache.set(message.content, count)
return count
}

export const countTokens = (enc: Tiktoken, messages: ChatMessage[]) => {
if (messages.length === 0) return

if (!enc) return { total: Infinity }

const lastMsg = messages.at(-1)
const context = messages.slice(0, -1)

const countTokens: (message: ChatMessage) => number = countTokensSingleMessage.bind(null, enc)
const countTokens: (cacheIt: boolean, message: ChatMessage) => number = countTokensSingleMessageWithCache.bind(null, enc)

const countLastMsg = countTokens(lastMsg!)
const countContext = context.map(countTokens).reduce((a, b) => a + b, 3) // im_start, "assistant", "\n"
const countLastMsg = countTokens(false, lastMsg!)
const countContext = context.map(countTokens.bind(null, true)).reduce((a, b) => a + b, 3) // im_start, "assistant", "\n"

return { countContext, countLastMsg, total: countContext + countLastMsg }
}
Expand Down
24 changes: 24 additions & 0 deletions sweep.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@

rules:
- "All new business logic should have corresponding unit tests."
- "Refactor large functions to be more modular."
- "Add docstrings to all functions and file headers."

branch: 'endless'

gha_enabled: True

description: 'The project is forked from @anse-app/chatgpt-demo, with an index site at https://free-chat.asia. The repository is mainly a TypeScript application using SolidJS and Svelte for the front end and UnoCSS for styling.'

draft: False

blocked_dirs: []

docs: []

sandbox:
install:
- trunk init
check:
- trunk fmt {file_path} || return 0
- trunk check --fix --print-failures {file_path}