Skip to content
Draft
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
5 changes: 5 additions & 0 deletions app/components/views/SettingsDrawer/RouteList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,11 @@ const getPaths = (remote: boolean): ButtonData[] => [
path: '/screens/ModelManagerScreen',
icon: 'folderopen',
},
{
name: 'Tools',
path: '/screens/ToolManagerScreen',
icon: 'tool',
},
{
name: 'TTS',
path: '/screens/TTSManagerScreen',
Expand Down
95 changes: 94 additions & 1 deletion app/screens/ChatScreen/ChatWindow/ChatBubble.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { useState } from 'react'
import { AppSettings } from '@lib/constants/GlobalValues'
import { useAppMode } from '@lib/state/AppMode'
import { Chats } from '@lib/state/Chat'
import { Theme } from '@lib/theme/ThemeManager'
import { ToolCallData } from 'db/schema'
import { Pressable, Text, View } from 'react-native'
import { useMMKVBoolean } from 'react-native-mmkv'
import { useShallow } from 'zustand/react/shallow'
Expand Down Expand Up @@ -44,7 +46,11 @@ const ChatBubble: React.FC<ChatTextProps> = ({
}

const hasSwipes = message?.swipes?.length > 1
const showSwipe = !message.is_user && isLastMessage && (hasSwipes || !isGreeting)
const isToolMessage = message.role === 'tool'
const showSwipe =
!message.is_user && !isToolMessage && isLastMessage && (hasSwipes || !isGreeting)
const currentSwipe = message.swipes[message.swipe_id]
const hasToolCalls = currentSwipe?.tool_calls && currentSwipe.tool_calls.length > 0
const timings = message.swipes[message.swipe_id].timings

return (
Expand Down Expand Up @@ -73,6 +79,23 @@ const ChatBubble: React.FC<ChatTextProps> = ({
],
}}
onLongPress={handleEnableEdit}>
{hasToolCalls && (
<ToolCallIndicator
toolCalls={currentSwipe.tool_calls!}
color={color}
fontSize={fontSize}
spacing={spacing}
borderRadius={borderRadius}
/>
)}
{isToolMessage && (
<ToolResultHeader
name={message.name}
color={color}
fontSize={fontSize}
spacing={spacing}
/>
)}
{isLastMessage ? (
<ChatTextLast nowGenerating={nowGenerating} index={index} />
) : (
Expand Down Expand Up @@ -115,4 +138,74 @@ const getFiniteValue = (value: number | null) => {
return value.toFixed(2)
}

type ToolCallIndicatorProps = {
toolCalls: ToolCallData[]
color: any
fontSize: any
spacing: any
borderRadius: any
}

const ToolCallIndicator = ({
toolCalls,
color,
fontSize,
spacing,
borderRadius,
}: ToolCallIndicatorProps) => {
const [expanded, setExpanded] = useState(false)
return (
<Pressable onPress={() => setExpanded(!expanded)}>
<View
style={{
backgroundColor: color.neutral._300,
borderRadius: borderRadius.s,
padding: spacing.sm,
marginBottom: spacing.sm,
}}>
<Text style={{ color: color.text._300, fontSize: fontSize.s, fontWeight: '600' }}>
{expanded ? '\u25BC' : '\u25B6'} Tool Call
{toolCalls.length > 1 ? 's' : ''}:{' '}
{toolCalls.map((tc) => tc.function.name).join(', ')}
</Text>
{expanded &&
toolCalls.map((tc, i) => (
<View key={tc.id || i} style={{ marginTop: spacing.sm }}>
<Text
style={{
color: color.text._400,
fontSize: fontSize.s,
fontFamily: 'monospace',
}}>
{tc.function.name}({tc.function.arguments})
</Text>
</View>
))}
</View>
</Pressable>
)
}

type ToolResultHeaderProps = {
name: string
color: any
fontSize: any
spacing: any
}

const ToolResultHeader = ({ name, color, fontSize, spacing }: ToolResultHeaderProps) => {
return (
<View
style={{
flexDirection: 'row',
alignItems: 'center',
marginBottom: spacing.sm,
}}>
<Text style={{ color: color.text._400, fontSize: fontSize.s, fontWeight: '600' }}>
{'\u2699\uFE0F'} Tool Result: {name}
</Text>
</View>
)
}

export default ChatBubble
132 changes: 132 additions & 0 deletions app/screens/ToolManagerScreen/AddTool.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import ThemedButton from '@components/buttons/ThemedButton'
import ThemedTextInput from '@components/input/ThemedTextInput'
import { Logger } from '@lib/state/Logger'
import { ToolState } from '@lib/state/ToolState'
import { Theme } from '@lib/theme/ThemeManager'
import { Stack, useRouter } from 'expo-router'
import { useState } from 'react'
import { ScrollView, StyleSheet, Text, View } from 'react-native'
import { SafeAreaView } from 'react-native-safe-area-context'

const PARAMS_PLACEHOLDER = `{"type":"object","properties":{"query":{"type":"string","description":"The search query"}},"required":["query"]}`

const AddTool = () => {
const styles = useStyles()
const router = useRouter()
const addTool = ToolState.useToolStore((state) => state.addTool)

const [name, setName] = useState('')
const [description, setDescription] = useState('')
const [parametersJson, setParametersJson] = useState('')
const [error, setError] = useState('')

const handleCreate = () => {
setError('')

if (!name.trim()) {
setError('Name is required')
return
}

if (!/^[a-z0-9_]+$/.test(name.trim())) {
setError('Name must be lowercase alphanumeric with underscores only')
return
}

if (!description.trim()) {
setError('Description is required')
return
}

let parsedParams: object
try {
parsedParams = JSON.parse(parametersJson.trim() || '{"type":"object","properties":{}}')
} catch (e) {
setError('Parameters must be valid JSON')
return
}

addTool({
name: name.trim(),
description: description.trim(),
parameters_schema: parsedParams,
enabled: true,
builtin: false,
character_id: null,
})

Logger.info(`Custom tool created: ${name.trim()}`)
router.back()
}

return (
<SafeAreaView edges={['bottom']} style={styles.mainContainer}>
<Stack.Screen options={{ title: 'Add Custom Tool' }} />
<ScrollView
style={{ flex: 1 }}
showsVerticalScrollIndicator={false}
contentContainerStyle={{ rowGap: 16, paddingBottom: 24 }}>
<ThemedTextInput
label="Function Name"
value={name}
onChangeText={setName}
placeholder="e.g. web_search"
/>
<Text style={styles.hintText}>
Lowercase letters, numbers, and underscores only
</Text>

<ThemedTextInput
label="Description"
value={description}
onChangeText={setDescription}
multiline
numberOfLines={3}
placeholder="Describe what this tool does for the model"
/>

<View>
<ThemedTextInput
label="Parameters (JSON Schema)"
value={parametersJson}
onChangeText={setParametersJson}
multiline
numberOfLines={6}
placeholder={PARAMS_PLACEHOLDER}
/>
<Text style={styles.hintText}>
JSON Schema defining the function's parameters. Leave empty for no
parameters.
</Text>
</View>

{error !== '' && <Text style={styles.errorText}>{error}</Text>}
</ScrollView>
<ThemedButton label="Create Tool" onPress={handleCreate} />
</SafeAreaView>
)
}

export default AddTool

const useStyles = () => {
const { color, spacing } = Theme.useTheme()
return StyleSheet.create({
mainContainer: {
marginVertical: spacing.xl,
paddingVertical: spacing.xl,
paddingHorizontal: spacing.xl,
flex: 1,
},

hintText: {
marginTop: spacing.s,
color: color.text._400,
},

errorText: {
color: color.error._400,
marginTop: spacing.s,
},
})
}
Loading