Skip to content

Commit

Permalink
Sortable and toggleable message options (#955)
Browse files Browse the repository at this point in the history
Co-authored-by: Sceuick <[email protected]>
  • Loading branch information
sceuick and sceuick committed Jun 27, 2024
1 parent 9ad2b4b commit 2b878b7
Show file tree
Hide file tree
Showing 17 changed files with 560 additions and 262 deletions.
1 change: 1 addition & 0 deletions common/adapters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ export const CLAUDE_CHAT_MODELS: Record<string, boolean> = {
[CLAUDE_MODELS.ClaudeV3_Opus]: true,
[CLAUDE_MODELS.ClaudeV3_Sonnet]: true,
[CLAUDE_MODELS.ClaudeV3_Haiku]: true,
[CLAUDE_MODELS.ClaudeV35_Sonnet]: true,
}

export const NOVEL_MODELS = {
Expand Down
16 changes: 10 additions & 6 deletions common/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@ import { AppSchema } from './types'

type Created = { _id: string; createdAt: string }

export type ChatTree = Record<
string,
{ depth: number; msg: AppSchema.ChatMessage; children: Set<string> }
>
export type ChatTree = Record<string, ChatNode>

export type ChatNode = {
depth: number
msg: AppSchema.ChatMessage
children: Set<string>
}

export type ChatDepths = Record<number, string[]>

export function toChatTree(messages: AppSchema.ChatMessage[]): ChatTree {
export function toChatGraph(messages: AppSchema.ChatMessage[]): { tree: ChatTree; root: string } {
const tree: ChatTree = {}

for (let i = 0; i < messages.length; i++) {
Expand All @@ -35,7 +39,7 @@ export function toChatTree(messages: AppSchema.ChatMessage[]): ChatTree {
parent.children.add(msg._id)
}

return tree
return { tree, root: messages[0]?._id || '' }
}

export function updateChatTree(tree: ChatTree, msg: AppSchema.ChatMessage) {
Expand Down
6 changes: 2 additions & 4 deletions common/presets/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,10 +113,8 @@ Then the roleplay chat between {{char}} and {{user}} begins.
{{#each msg}}{{#if .isbot}}<bot>{{.name}}: {{.msg}}</bot>{{/if}}{{#if .isuser}}<user>{{.name}}: {{.msg}}</user>{{/if}}
{{/each}}
{{#if ujb}}<bot>
{{ujb}}</bot>
{{/if}}
<bot>{{post}}`,
<bot>{{#if ujb}}({{ujb}}) {{/if}}{{post}}`,
Alpaca: neat`
{{#if system_prompt}}{{system_prompt}}
{{/if}}
Expand Down
11 changes: 11 additions & 0 deletions common/types/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ export type CustomUI = {
chatQuoteColor: string
}

export type MessageOption = 'edit' | 'regen' | 'trash' | 'fork' | 'prompt'

export type UISettings = {
theme: string
themeBg?: string
Expand All @@ -59,6 +61,7 @@ export type UISettings = {
/** 0 -> 1. 0 = transparent. 1 = opaque */
msgOpacity: number
mobileSendOnEnter: boolean
msgOptsInline: { [key in MessageOption]: { outer: boolean; pos: number } }

viewMode?: 'split' | 'standard'
viewHeight?: number
Expand Down Expand Up @@ -137,4 +140,12 @@ export const defaultUIsettings: UISettings = {
chatEmphasisColor: '--text-600',
chatQuoteColor: '--text-800',
},

msgOptsInline: {
edit: { outer: true, pos: 0 },
prompt: { outer: false, pos: 3 },
fork: { outer: false, pos: 2 },
regen: { outer: true, pos: 1 },
trash: { outer: false, pos: 4 },
},
}
2 changes: 0 additions & 2 deletions web/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import Redirect from './shared/Redirect'
import Maintenance from './shared/Maintenance'
import CharacterChats from './pages/Character/ChatList'
import ChatDetail from './pages/Chat/ChatDetail'
import ChangeLog from './pages/Home/ChangeLog'
import Settings from './pages/Settings'
import ProfilePage, { ProfileModal } from './pages/Profile'
import { usePaneManager } from './shared/hooks'
Expand Down Expand Up @@ -63,7 +62,6 @@ const App: Component = () => {
</Show>
<Route path="/chat/:id" component={ChatDetail} />
<Route path={['/info', '/']} component={HomePage} />
<Route path="/changelog" component={ChangeLog} />
<Route path="/presets/:id" component={lazy(() => import('./pages/GenerationPresets'))} />
<Route
path="/presets"
Expand Down
16 changes: 0 additions & 16 deletions web/pages/Chat/ChatOptions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,6 @@ const ChatOptions: Component<{
chatStore.option({ hideOoc: !chats.opts.hideOoc })
}

const toggleEditing = () => {
chatStore.option({ editing: !chats.opts.editing })
}

const toggleScreenshot = (value: boolean) => {
chatStore.option({ screenshot: value })
}
Expand Down Expand Up @@ -108,18 +104,6 @@ const ChatOptions: Component<{
</Option>
</Show>

<Option onClick={toggleEditing} hide={!isOwner()}>
<div class="flex w-full items-center justify-between">
<div>Enable Chat Editing</div>
<Toggle
class="flex items-center"
fieldName="editChat"
value={chats.opts.editing}
onChange={toggleEditing}
/>
</div>
</Option>

<Option onClick={() => props.togglePane('character')} hide={!isOwner()}>
<User /> Main Character
</Option>
Expand Down
174 changes: 153 additions & 21 deletions web/pages/Chat/components/ChatGraph.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,29 @@
import cyto from 'cytoscape'
import dagre from 'cytoscape-dagre'
import { Component, createEffect, createSignal, on, onCleanup } from 'solid-js'
import { ChatTree } from '/common/chat'
import { ChatNode, ChatTree } from '/common/chat'
import { msgStore } from '/web/store'
import { getSettingColor } from '/web/shared/colors'

export { ChatGraph as default }

cyto.use(dagre)

export const ChatGraph: Component<{ tree: ChatTree; leafId: string; dir?: string }> = (props) => {
export const ChatGraph: Component<{ leafId: string; dir?: string; nodes: 'short' | 'full' }> = (
props
) => {
let cyRef: any

const [graph, setGraph] = createSignal<cyto.Core>()
const graph = msgStore((s) => s.graph)

const [instance, setInstance] = createSignal<cyto.Core>()
const init = (ref: HTMLDivElement, tree: ChatTree) => {
cyRef = ref
redraw(tree)
}

const redraw = (tree: ChatTree) => {
graph()?.destroy()
instance()?.destroy()

const cy = cyto({
container: cyRef,
Expand All @@ -29,17 +33,28 @@ export const ChatGraph: Component<{ tree: ChatTree; leafId: string; dir?: string
{
selector: 'edge',
style: {
width: 4,
width: 2,
'target-arrow-shape': 'triangle',
'line-color': '#fff',
'target-arrow-color': '#fff',
'line-color': getSettingColor('bg-600'),
'target-arrow-color': getSettingColor('bg-600'),
'curve-style': 'bezier',
'text-valign': 'bottom',
},
},
{
selector: 'edge[label]',
style: {
color: getSettingColor('hl-500'),
label: 'data(label)',
'text-valign': 'top',
'font-weight': 600,
},
},

{
selector: 'node[label]',
style: {
color: getSettingColor('bg-500'),
color: getSettingColor('bg-400'),
'text-valign': 'bottom',
label: 'data(label)',
},
Expand All @@ -48,7 +63,10 @@ export const ChatGraph: Component<{ tree: ChatTree; leafId: string; dir?: string

panningEnabled: true,
wheelSensitivity: 0.1,
elements: getElements(props.tree, props.leafId),
elements:
props.nodes === 'full'
? getAllElements(graph.tree, props.leafId)
: getElements(graph.tree, graph.root, props.leafId),
})

cy.on('click', 'node', function (this: any, evt) {
Expand All @@ -67,27 +85,39 @@ export const ChatGraph: Component<{ tree: ChatTree; leafId: string; dir?: string
cy.fit()
cy.center()

setGraph(cy)
setInstance(cy)
return cy
}

createEffect(
on(
() => props.tree,
() => graph.tree,
(tree) => {
const cy = graph()
const cy = instance()
if (!cy) return

redraw(tree)
}
)
)

createEffect(
on(
() => props.nodes,
() => {
const cy = instance()
if (!cy) return

redraw(graph.tree)
}
)
)

createEffect(
on(
() => props.leafId,
(leafId) => {
const cy = graph()
const cy = instance()
if (!cy) return

const nodes = cy.nodes()
Expand All @@ -104,28 +134,72 @@ export const ChatGraph: Component<{ tree: ChatTree; leafId: string; dir?: string
on(
() => props.dir,
(dir) => {
const result = graph()?.layout({ name: 'dagre', rankDir: dir || 'LR' } as any)
const result = instance()?.layout({ name: 'dagre', rankDir: dir || 'LR' } as any)
result?.run()
graph()?.fit()
instance()?.fit()
}
)
)

onCleanup(() => {
graph()?.destroy()
instance()?.destroy()
})

return (
<div
class="flex h-full max-h-[400px] min-h-[400px] w-full justify-center p-4"
ref={(ref) => init(ref, props.tree)}
>
{/* <div class="left-0 top-0 z-50 h-full w-full" ref={(ref) => init(ref, props.tree)}></div> */}
</div>
ref={(ref) => init(ref, graph.tree)}
></div>
)
}

function getElements(tree: ChatTree, leafId: string) {
function getElements(tree: ChatTree, root: string, leafId: string) {
const elements: cyto.ElementDefinition[] = []

const short = getShorthandTree(tree, root)
const visited = new Set<string>()
for (const {
start: { msg: smsg },
end: { msg: emsg },
length,
} of short) {
if (!visited.has(smsg._id)) {
visited.add(smsg._id)
elements.push({
group: 'nodes',
data: { id: smsg._id, label: smsg._id.slice(0, 3) },
style: {
shape: smsg._id === root ? 'star' : 'ellipse',
'background-color':
root === smsg._id || leafId === smsg._id
? getSettingColor('hl-500')
: getSettingColor('bg-500'),
},
})
}

if (!visited.has(emsg._id)) {
visited.add(emsg._id)
elements.push({
group: 'nodes',
data: { id: emsg._id, label: emsg._id.slice(0, 3) },
style: {
'background-color':
leafId === emsg._id ? getSettingColor('hl-500') : getSettingColor('bg-500'),
},
})
}

elements.push({
group: 'edges',
data: { source: smsg._id, target: emsg._id, label: `${length}` },
})
}

return elements
}

function getAllElements(tree: ChatTree, leafId: string) {
const elements: cyto.ElementDefinition[] = []

for (const node of Object.values(tree)) {
Expand All @@ -152,3 +226,61 @@ function getElements(tree: ChatTree, leafId: string) {

return elements
}

type PathSkip = { start: ChatNode; end: ChatNode; length: number }

function getShorthandTree(tree: ChatTree, root: string) {
const edges: Array<PathSkip> = []
const skips = getPathSkips(tree, root)

if (!skips) return edges

if (Array.isArray(skips)) {
edges.push(...skips)
for (const skip of skips) {
const subskips = getShorthandTree(tree, skip.end.msg._id)
edges.push(...subskips)
}
} else {
edges.push(skips)
}

return edges
}

function getPathSkips(tree: ChatTree, id: string): PathSkip | PathSkip[] | undefined {
const start = tree[id]

const visited = new Set<string>([id])
let curr = tree[id]
if (!curr) return
let length = 1

do {
if (curr.children.size === 0) {
if (id === curr.msg._id) return
return { start, end: curr, length }
}

if (curr.children.size === 1) {
const next = curr.children.values().next().value
if (visited.has(next)) {
throw new Error(`Invalid chat tree: Contains a circular reference (${next})`)
}

length++
visited.add(next)
curr = tree[next]
continue
}

const paths: PathSkip[] = []
for (const child of curr.children) {
const end = tree[child]
if (!end) continue

paths.push({ start, end, length })
}
return paths
} while (true)
}
Loading

0 comments on commit 2b878b7

Please sign in to comment.