refactor(app): simplify agent list UI and remove unused sidebar component
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Claude</title><path d="M4.709 15.955l4.72-2.647.08-.23-.08-.128H9.2l-.79-.048-2.698-.073-2.339-.097-2.266-.122-.571-.121L0 11.784l.055-.352.48-.321.686.06 1.52.103 2.278.158 1.652.097 2.449.255h.389l.055-.157-.134-.098-.103-.097-2.358-1.596-2.552-1.688-1.336-.972-.724-.491-.364-.462-.158-1.008.656-.722.881.06.225.061.893.686 1.908 1.476 2.491 1.833.365.304.145-.103.019-.073-.164-.274-1.355-2.446-1.446-2.49-.644-1.032-.17-.619a2.97 2.97 0 01-.104-.729L6.283.134 6.696 0l.996.134.42.364.62 1.414 1.002 2.229 1.555 3.03.456.898.243.832.091.255h.158V9.01l.128-1.706.237-2.095.23-2.695.08-.76.376-.91.747-.492.584.28.48.685-.067.444-.286 1.851-.559 2.903-.364 1.942h.212l.243-.242.985-1.306 1.652-2.064.73-.82.85-.904.547-.431h1.033l.76 1.129-.34 1.166-1.064 1.347-.881 1.142-1.264 1.7-.79 1.36.073.11.188-.02 2.856-.606 1.543-.28 1.841-.315.833.388.091.395-.328.807-1.969.486-2.309.462-3.439.813-.042.03.049.061 1.549.146.662.036h1.622l3.02.225.79.522.474.638-.079.485-1.215.62-1.64-.389-3.829-.91-1.312-.329h-.182v.11l1.093 1.068 2.006 1.81 2.509 2.33.127.578-.322.455-.34-.049-2.205-1.657-.851-.747-1.926-1.62h-.128v.17l.444.649 2.345 3.521.122 1.08-.17.353-.608.213-.668-.122-1.374-1.925-1.415-2.167-1.143-1.943-.14.08-.674 7.254-.316.37-.729.28-.607-.461-.322-.747.322-1.476.389-1.924.315-1.53.286-1.9.17-.632-.012-.042-.14.018-1.434 1.967-2.18 2.945-1.726 1.845-.414.164-.717-.37.067-.662.401-.589 2.388-3.036 1.44-1.882.93-1.086-.006-.158h-.055L4.132 18.56l-1.13.146-.487-.456.061-.746.231-.243 1.908-1.312-.006.006z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>OpenAI</title><path d="M21.55 10.004a5.416 5.416 0 00-.478-4.501c-1.217-2.09-3.662-3.166-6.05-2.66A5.59 5.59 0 0010.831 1C8.39.995 6.224 2.546 5.473 4.838A5.553 5.553 0 001.76 7.496a5.487 5.487 0 00.691 6.5 5.416 5.416 0 00.477 4.502c1.217 2.09 3.662 3.165 6.05 2.66A5.586 5.586 0 0013.168 23c2.443.006 4.61-1.546 5.361-3.84a5.553 5.553 0 003.715-2.66 5.488 5.488 0 00-.693-6.497v.001zm-8.381 11.558a4.199 4.199 0 01-2.675-.954c.034-.018.093-.05.132-.074l4.44-2.53a.71.71 0 00.364-.623v-6.176l1.877 1.069c.02.01.033.029.036.05v5.115c-.003 2.274-1.87 4.118-4.174 4.123zM4.192 17.78a4.059 4.059 0 01-.498-2.763c.032.02.09.055.131.078l4.44 2.53c.225.13.504.13.73 0l5.42-3.088v2.138a.068.068 0 01-.027.057L9.9 19.288c-1.999 1.136-4.552.46-5.707-1.51h-.001zM3.023 8.216A4.15 4.15 0 015.198 6.41l-.002.151v5.06a.711.711 0 00.364.624l5.42 3.087-1.876 1.07a.067.067 0 01-.063.005l-4.489-2.559c-1.995-1.14-2.679-3.658-1.53-5.63h.001zm15.417 3.54l-5.42-3.088L14.896 7.6a.067.067 0 01.063-.006l4.489 2.557c1.998 1.14 2.683 3.662 1.529 5.633a4.163 4.163 0 01-2.174 1.807V12.38a.71.71 0 00-.363-.623zm1.867-2.773a6.04 6.04 0 00-.132-.078l-4.44-2.53a.731.731 0 00-.729 0l-5.42 3.088V7.325a.068.068 0 01.027-.057L14.1 4.713c2-1.137 4.555-.46 5.707 1.513.487.833.664 1.809.499 2.757h.001zm-11.741 3.81l-1.877-1.068a.065.065 0 01-.036-.051V6.559c.001-2.277 1.873-4.122 4.181-4.12.976 0 1.92.338 2.671.954-.034.018-.092.05-.131.073l-4.44 2.53a.71.71 0 00-.365.623l-.003 6.173v.002zm1.02-2.168L12 9.25l2.414 1.375v2.75L12 14.75l-2.415-1.375v-2.75z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -673,7 +673,7 @@ function AgentScreenContent({
|
|||||||
rightContent={
|
rightContent={
|
||||||
<View ref={menuButtonRef} collapsable={false}>
|
<View ref={menuButtonRef} collapsable={false}>
|
||||||
<Pressable onPress={handleOpenMenu} style={styles.menuButton}>
|
<Pressable onPress={handleOpenMenu} style={styles.menuButton}>
|
||||||
<MoreVertical size={20} color={theme.colors.foreground} />
|
<MoreVertical size={20} color={theme.colors.mutedForeground} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,8 +22,13 @@ import { AgentStatusBar } from "./agent-status-bar";
|
|||||||
import { RealtimeControls } from "./realtime-controls";
|
import { RealtimeControls } from "./realtime-controls";
|
||||||
import { useImageAttachmentPicker } from "@/hooks/use-image-attachment-picker";
|
import { useImageAttachmentPicker } from "@/hooks/use-image-attachment-picker";
|
||||||
import { useSessionStore } from "@/stores/session-store";
|
import { useSessionStore } from "@/stores/session-store";
|
||||||
import { MessageInput, type MessagePayload, type ImageAttachment } from "./message-input";
|
import {
|
||||||
|
MessageInput,
|
||||||
|
type MessagePayload,
|
||||||
|
type ImageAttachment,
|
||||||
|
} from "./message-input";
|
||||||
import type { UseWebSocketReturn } from "@/hooks/use-websocket";
|
import type { UseWebSocketReturn } from "@/hooks/use-websocket";
|
||||||
|
import { Theme } from "@/styles/theme";
|
||||||
|
|
||||||
type QueuedMessage = {
|
type QueuedMessage = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -48,8 +53,12 @@ const MAX_INPUT_WIDTH = 960;
|
|||||||
// Android currently crashes inside ViewGroup.dispatchDraw when running Reanimated
|
// Android currently crashes inside ViewGroup.dispatchDraw when running Reanimated
|
||||||
// entering/exiting animations (see react-native-reanimated#8422), so guard them.
|
// entering/exiting animations (see react-native-reanimated#8422), so guard them.
|
||||||
const SHOULD_DISABLE_ENTRY_EXIT_ANIMATIONS = Platform.OS === "android";
|
const SHOULD_DISABLE_ENTRY_EXIT_ANIMATIONS = Platform.OS === "android";
|
||||||
const REALTIME_FADE_IN = SHOULD_DISABLE_ENTRY_EXIT_ANIMATIONS ? undefined : FadeIn.duration(250);
|
const REALTIME_FADE_IN = SHOULD_DISABLE_ENTRY_EXIT_ANIMATIONS
|
||||||
const REALTIME_FADE_OUT = SHOULD_DISABLE_ENTRY_EXIT_ANIMATIONS ? undefined : FadeOut.duration(250);
|
? undefined
|
||||||
|
: FadeIn.duration(250);
|
||||||
|
const REALTIME_FADE_OUT = SHOULD_DISABLE_ENTRY_EXIT_ANIMATIONS
|
||||||
|
? undefined
|
||||||
|
: FadeOut.duration(250);
|
||||||
|
|
||||||
export function AgentInputArea({
|
export function AgentInputArea({
|
||||||
agentId,
|
agentId,
|
||||||
@@ -219,7 +228,11 @@ export function AgentInputArea({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleSubmit(payload: MessagePayload) {
|
function handleSubmit(payload: MessagePayload) {
|
||||||
void sendMessageWithContent(payload.text, payload.images, payload.forceSend);
|
void sendMessageWithContent(
|
||||||
|
payload.text,
|
||||||
|
payload.images,
|
||||||
|
payload.forceSend
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handlePickImage() {
|
async function handlePickImage() {
|
||||||
@@ -262,7 +275,8 @@ export function AgentInputArea({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const existing = getDraftInput(agentId);
|
const existing = getDraftInput(agentId);
|
||||||
const isSameText = existing?.text === userInput;
|
const isSameText = existing?.text === userInput;
|
||||||
const existingImages: ImageAttachment[] = (existing?.images ?? []) as ImageAttachment[];
|
const existingImages: ImageAttachment[] = (existing?.images ??
|
||||||
|
[]) as ImageAttachment[];
|
||||||
const isSameImages =
|
const isSameImages =
|
||||||
existingImages.length === selectedImages.length &&
|
existingImages.length === selectedImages.length &&
|
||||||
existingImages.every((img, idx) => {
|
existingImages.every((img, idx) => {
|
||||||
@@ -387,17 +401,16 @@ export function AgentInputArea({
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
const leftContent = (
|
const leftContent = <AgentStatusBar agentId={agentId} serverId={serverId} />;
|
||||||
<AgentStatusBar agentId={agentId} serverId={serverId} />
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Animated.View
|
<Animated.View
|
||||||
style={[styles.container, { paddingBottom: insets.bottom }, keyboardAnimatedStyle]}
|
style={[
|
||||||
|
styles.container,
|
||||||
|
{ paddingBottom: insets.bottom },
|
||||||
|
keyboardAnimatedStyle,
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
{/* Border separator */}
|
|
||||||
<View style={styles.borderSeparator} />
|
|
||||||
|
|
||||||
{/* Realtime controls - only when active */}
|
{/* Realtime controls - only when active */}
|
||||||
{isRealtimeMode && (
|
{isRealtimeMode && (
|
||||||
<Animated.View
|
<Animated.View
|
||||||
@@ -469,11 +482,12 @@ export function AgentInputArea({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create(((theme: any) => ({
|
const BUTTON_SIZE = 40;
|
||||||
|
|
||||||
|
const styles = StyleSheet.create(((theme: Theme) => ({
|
||||||
container: {
|
container: {
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
position: "relative",
|
position: "relative",
|
||||||
backgroundColor: theme.colors.background,
|
|
||||||
},
|
},
|
||||||
borderSeparator: {
|
borderSeparator: {
|
||||||
height: theme.borderWidth[1],
|
height: theme.borderWidth[1],
|
||||||
@@ -485,21 +499,20 @@ const styles = StyleSheet.create(((theme: any) => ({
|
|||||||
inputAreaContainer: {
|
inputAreaContainer: {
|
||||||
position: "relative",
|
position: "relative",
|
||||||
minHeight: FOOTER_HEIGHT,
|
minHeight: FOOTER_HEIGHT,
|
||||||
|
marginHorizontal: "auto",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
backgroundColor: theme.colors.background,
|
padding: theme.spacing[4],
|
||||||
},
|
},
|
||||||
inputAreaContent: {
|
inputAreaContent: {
|
||||||
width: "100%",
|
width: "100%",
|
||||||
maxWidth: MAX_INPUT_WIDTH,
|
maxWidth: MAX_INPUT_WIDTH,
|
||||||
paddingHorizontal: theme.spacing[4],
|
|
||||||
paddingVertical: theme.spacing[3],
|
|
||||||
gap: theme.spacing[3],
|
gap: theme.spacing[3],
|
||||||
},
|
},
|
||||||
realtimeButton: {
|
realtimeButton: {
|
||||||
width: 40,
|
width: 34,
|
||||||
height: 40,
|
height: 34,
|
||||||
borderRadius: theme.borderRadius.full,
|
borderRadius: theme.borderRadius.full,
|
||||||
backgroundColor: theme.colors.accentForeground,
|
backgroundColor: theme.colors.accentForeground,
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
@@ -509,8 +522,8 @@ const styles = StyleSheet.create(((theme: any) => ({
|
|||||||
backgroundColor: theme.colors.palette.blue[600],
|
backgroundColor: theme.colors.palette.blue[600],
|
||||||
},
|
},
|
||||||
cancelButton: {
|
cancelButton: {
|
||||||
width: 40,
|
width: BUTTON_SIZE,
|
||||||
height: 40,
|
height: BUTTON_SIZE,
|
||||||
borderRadius: theme.borderRadius.full,
|
borderRadius: theme.borderRadius.full,
|
||||||
backgroundColor: theme.colors.palette.red[500],
|
backgroundColor: theme.colors.palette.red[500],
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { useCallback, useMemo, useState } from "react";
|
|||||||
import { router, usePathname } from "expo-router";
|
import { router, usePathname } from "expo-router";
|
||||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
import { formatTimeAgo } from "@/utils/time";
|
import { formatTimeAgo } from "@/utils/time";
|
||||||
import { getAgentProviderDefinition } from "@server/server/agent/provider-manifest";
|
|
||||||
import { type AggregatedAgent } from "@/hooks/use-aggregated-agents";
|
import { type AggregatedAgent } from "@/hooks/use-aggregated-agents";
|
||||||
import { useSessionStore } from "@/stores/session-store";
|
import { useSessionStore } from "@/stores/session-store";
|
||||||
import { buildAgentNavigationKey, startNavigationTiming } from "@/utils/navigation-timing";
|
import { buildAgentNavigationKey, startNavigationTiming } from "@/utils/navigation-timing";
|
||||||
@@ -89,81 +88,47 @@ export function AgentList({ agents, isRefreshing = false, onRefresh, selectedAge
|
|||||||
const renderAgentItem = useCallback<ListRenderItem<AggregatedAgent>>(
|
const renderAgentItem = useCallback<ListRenderItem<AggregatedAgent>>(
|
||||||
({ item: agent }) => {
|
({ item: agent }) => {
|
||||||
const timeAgo = formatTimeAgo(agent.lastActivityAt);
|
const timeAgo = formatTimeAgo(agent.lastActivityAt);
|
||||||
const providerLabel = getAgentProviderDefinition(agent.provider).label;
|
|
||||||
const isRunning = agent.status === "running";
|
const isRunning = agent.status === "running";
|
||||||
const isSelected = selectedAgentId === agent.id;
|
const isSelected = selectedAgentId === agent.id;
|
||||||
|
const statusColor = isRunning ? "#3b82f6" : agent.requiresAttention ? "#22c55e" : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
style={({ pressed }) => [
|
style={({ pressed, hovered }) => [
|
||||||
styles.agentItem,
|
styles.agentItem,
|
||||||
isSelected && styles.agentItemSelected,
|
isSelected && styles.agentItemSelected,
|
||||||
|
hovered && styles.agentItemHovered,
|
||||||
pressed && styles.agentItemPressed,
|
pressed && styles.agentItemPressed,
|
||||||
]}
|
]}
|
||||||
onPress={() => handleAgentPress(agent.serverId, agent.id)}
|
onPress={() => handleAgentPress(agent.serverId, agent.id)}
|
||||||
onLongPress={() => handleAgentLongPress(agent)}
|
onLongPress={() => handleAgentLongPress(agent)}
|
||||||
>
|
>
|
||||||
|
{({ hovered }) => (
|
||||||
<View style={styles.agentContent}>
|
<View style={styles.agentContent}>
|
||||||
<View style={styles.titleRow}>
|
<View style={styles.row}>
|
||||||
|
{statusColor && (
|
||||||
|
<View style={[styles.statusDot, { backgroundColor: statusColor }]} />
|
||||||
|
)}
|
||||||
<Text
|
<Text
|
||||||
style={styles.agentTitle}
|
style={[
|
||||||
|
styles.agentTitle,
|
||||||
|
(isSelected || hovered) && styles.agentTitleHighlighted,
|
||||||
|
]}
|
||||||
numberOfLines={1}
|
numberOfLines={1}
|
||||||
>
|
>
|
||||||
{agent.title || "New Agent"}
|
{agent.title || "New Agent"}
|
||||||
</Text>
|
</Text>
|
||||||
<View style={[styles.hostBadge, { backgroundColor: theme.colors.muted }]}>
|
|
||||||
<Text
|
|
||||||
style={[styles.hostText, { color: theme.colors.mutedForeground }]}
|
|
||||||
numberOfLines={1}
|
|
||||||
>
|
|
||||||
{agent.serverLabel}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<Text style={styles.agentDirectory} numberOfLines={1}>
|
<Text style={styles.secondaryRow} numberOfLines={1}>
|
||||||
{agent.cwd}
|
{agent.cwd.replace(/^\/(?:Users|home)\/[^/]+/, "~")} · {timeAgo}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<View style={styles.statusRow}>
|
|
||||||
<View style={styles.statusGroup}>
|
|
||||||
<View
|
|
||||||
style={[styles.providerBadge, { backgroundColor: theme.colors.muted }]}
|
|
||||||
>
|
|
||||||
<Text
|
|
||||||
style={[styles.providerText, { color: theme.colors.mutedForeground }]}
|
|
||||||
numberOfLines={1}
|
|
||||||
>
|
|
||||||
{providerLabel}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{isRunning ? (
|
|
||||||
<View style={styles.statusBadge}>
|
|
||||||
<View style={[styles.statusDot, { backgroundColor: "#3b82f6" }]} />
|
|
||||||
<Text style={[styles.statusText, { color: "#3b82f6" }]}>
|
|
||||||
Running
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
) : agent.requiresAttention ? (
|
|
||||||
<View style={styles.statusBadge}>
|
|
||||||
<View style={[styles.statusDot, { backgroundColor: "#22c55e" }]} />
|
|
||||||
<Text style={[styles.statusText, { color: "#22c55e" }]}>
|
|
||||||
Finished
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
) : null}
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<Text style={styles.timeAgo}>
|
|
||||||
{timeAgo}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
|
)}
|
||||||
</Pressable>
|
</Pressable>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[handleAgentLongPress, handleAgentPress, selectedAgentId, theme.colors.muted, theme.colors.mutedForeground, theme.colors.primary, theme.colors.primaryForeground]
|
[handleAgentLongPress, handleAgentPress, selectedAgentId]
|
||||||
);
|
);
|
||||||
|
|
||||||
const keyExtractor = useCallback(
|
const keyExtractor = useCallback(
|
||||||
@@ -254,89 +219,52 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
paddingBottom: theme.spacing[4],
|
paddingBottom: theme.spacing[4],
|
||||||
},
|
},
|
||||||
agentItem: {
|
agentItem: {
|
||||||
paddingVertical: theme.spacing[4],
|
paddingVertical: theme.spacing[3],
|
||||||
paddingHorizontal: theme.spacing[4],
|
paddingHorizontal: theme.spacing[3],
|
||||||
borderRadius: theme.borderRadius.lg,
|
borderRadius: theme.borderRadius.lg,
|
||||||
marginBottom: theme.spacing[2],
|
marginBottom: theme.spacing[2],
|
||||||
backgroundColor: theme.colors.muted,
|
backgroundColor: theme.colors.palette.zinc[950],
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: "transparent",
|
|
||||||
},
|
},
|
||||||
agentItemSelected: {
|
agentItemSelected: {
|
||||||
backgroundColor: theme.colors.accent,
|
backgroundColor: theme.colors.palette.zinc[800],
|
||||||
borderColor: theme.colors.primary,
|
},
|
||||||
|
agentItemHovered: {
|
||||||
|
backgroundColor: theme.colors.palette.zinc[850],
|
||||||
},
|
},
|
||||||
agentItemPressed: {
|
agentItemPressed: {
|
||||||
opacity: 0.7,
|
backgroundColor: theme.colors.palette.zinc[800],
|
||||||
backgroundColor: theme.colors.accent,
|
|
||||||
},
|
},
|
||||||
agentContent: {
|
agentContent: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
gap: theme.spacing[1],
|
||||||
},
|
},
|
||||||
titleRow: {
|
row: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "space-between",
|
|
||||||
gap: theme.spacing[2],
|
gap: theme.spacing[2],
|
||||||
marginBottom: theme.spacing[1],
|
|
||||||
},
|
},
|
||||||
agentTitle: {
|
agentTitle: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
fontSize: theme.fontSize.base,
|
fontSize: theme.fontSize.base,
|
||||||
fontWeight: theme.fontWeight.semibold,
|
fontWeight: {
|
||||||
|
xs: "400",
|
||||||
|
md: "300",
|
||||||
|
},
|
||||||
|
color: theme.colors.palette.zinc[200],
|
||||||
|
},
|
||||||
|
agentTitleHighlighted: {
|
||||||
color: theme.colors.foreground,
|
color: theme.colors.foreground,
|
||||||
},
|
},
|
||||||
hostBadge: {
|
secondaryRow: {
|
||||||
borderRadius: theme.borderRadius.full,
|
|
||||||
paddingHorizontal: theme.spacing[2],
|
|
||||||
paddingVertical: 2,
|
|
||||||
},
|
|
||||||
hostText: {
|
|
||||||
fontSize: theme.fontSize.xs,
|
|
||||||
fontWeight: theme.fontWeight.normal,
|
|
||||||
},
|
|
||||||
agentDirectory: {
|
|
||||||
fontSize: theme.fontSize.sm,
|
fontSize: theme.fontSize.sm,
|
||||||
|
fontWeight: "300",
|
||||||
color: theme.colors.mutedForeground,
|
color: theme.colors.mutedForeground,
|
||||||
marginBottom: theme.spacing[1],
|
|
||||||
},
|
|
||||||
statusRow: {
|
|
||||||
flexDirection: "row",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
gap: theme.spacing[2],
|
|
||||||
},
|
|
||||||
statusGroup: {
|
|
||||||
flexDirection: "row",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: theme.spacing[2],
|
|
||||||
},
|
|
||||||
providerBadge: {
|
|
||||||
borderRadius: theme.borderRadius.full,
|
|
||||||
},
|
|
||||||
providerText: {
|
|
||||||
fontSize: theme.fontSize.xs,
|
|
||||||
fontWeight: theme.fontWeight.semibold,
|
|
||||||
textTransform: "uppercase",
|
|
||||||
},
|
|
||||||
statusBadge: {
|
|
||||||
flexDirection: "row",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: theme.spacing[1],
|
|
||||||
},
|
},
|
||||||
statusDot: {
|
statusDot: {
|
||||||
width: 6,
|
width: 8,
|
||||||
height: 6,
|
height: 8,
|
||||||
borderRadius: theme.borderRadius.full,
|
borderRadius: theme.borderRadius.full,
|
||||||
},
|
},
|
||||||
statusText: {
|
|
||||||
fontSize: theme.fontSize.xs,
|
|
||||||
fontWeight: theme.fontWeight.semibold,
|
|
||||||
},
|
|
||||||
timeAgo: {
|
|
||||||
fontSize: theme.fontSize.xs,
|
|
||||||
color: theme.colors.mutedForeground,
|
|
||||||
},
|
|
||||||
sheetOverlay: {
|
sheetOverlay: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
justifyContent: "flex-end",
|
justifyContent: "flex-end",
|
||||||
|
|||||||
@@ -1,350 +0,0 @@
|
|||||||
import { View, Text, Pressable, ScrollView } from "react-native";
|
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
||||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
|
||||||
import { Plus, X } from "lucide-react-native";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { GestureDetector, Gesture } from "react-native-gesture-handler";
|
|
||||||
import Animated, {
|
|
||||||
useSharedValue,
|
|
||||||
useAnimatedStyle,
|
|
||||||
withTiming,
|
|
||||||
cancelAnimation,
|
|
||||||
runOnJS,
|
|
||||||
Easing,
|
|
||||||
type SharedValue,
|
|
||||||
} from "react-native-reanimated";
|
|
||||||
import type { Agent } from "@/contexts/session-context";
|
|
||||||
import { getAgentStatusColor, getAgentStatusLabel } from "@/utils/agent-status";
|
|
||||||
import { getAgentProviderDefinition } from "@server/server/agent/provider-manifest";
|
|
||||||
|
|
||||||
interface AgentSidebarProps {
|
|
||||||
isOpen: boolean;
|
|
||||||
agents: Agent[];
|
|
||||||
activeAgentId: string | null;
|
|
||||||
onClose: () => void;
|
|
||||||
onSelectAgent: (serverId: string, agentId: string) => void;
|
|
||||||
onNewAgent: () => void;
|
|
||||||
edgeSwipeTranslateX?: SharedValue<number> | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function AgentSidebar({
|
|
||||||
isOpen,
|
|
||||||
agents,
|
|
||||||
activeAgentId,
|
|
||||||
onClose,
|
|
||||||
onSelectAgent,
|
|
||||||
onNewAgent,
|
|
||||||
edgeSwipeTranslateX,
|
|
||||||
}: AgentSidebarProps) {
|
|
||||||
const { theme } = useUnistyles();
|
|
||||||
const insets = useSafeAreaInsets();
|
|
||||||
const savedTranslateX = useSharedValue(0);
|
|
||||||
const [isVisible, setIsVisible] = useState(isOpen);
|
|
||||||
const fallbackTranslateX = useSharedValue(-300);
|
|
||||||
const translateX = edgeSwipeTranslateX ?? fallbackTranslateX;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isOpen) {
|
|
||||||
setIsVisible(true);
|
|
||||||
// Only animate if starting from fully closed position (not mid-animation from edge swipe)
|
|
||||||
if (translateX.value === -300) {
|
|
||||||
translateX.value = withTiming(0, {
|
|
||||||
duration: 150,
|
|
||||||
easing: Easing.out(Easing.ease),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Only animate if starting from fully open position
|
|
||||||
if (translateX.value === 0) {
|
|
||||||
translateX.value = withTiming(-300, {
|
|
||||||
duration: 150,
|
|
||||||
easing: Easing.out(Easing.ease),
|
|
||||||
}, (finished) => {
|
|
||||||
if (finished) {
|
|
||||||
runOnJS(setIsVisible)(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (translateX.value === -300) {
|
|
||||||
// Already closed, just hide
|
|
||||||
setIsVisible(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [isOpen]);
|
|
||||||
|
|
||||||
function handleAgentSelect(agentId: string, serverId: string) {
|
|
||||||
onSelectAgent(serverId, agentId);
|
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pan gesture for closing sidebar (swipe left on the sidebar)
|
|
||||||
const closeGesture = Gesture.Pan()
|
|
||||||
.enabled(isOpen) // Only active when sidebar is open
|
|
||||||
.activeOffsetX(-10) // Require leftward swipe
|
|
||||||
.onBegin(() => {
|
|
||||||
cancelAnimation(translateX);
|
|
||||||
savedTranslateX.value = translateX.value;
|
|
||||||
})
|
|
||||||
.onChange((event) => {
|
|
||||||
// Directly update translateX - clamp between -300 and 0
|
|
||||||
const newTranslateX = Math.max(-300, Math.min(0, savedTranslateX.value + event.translationX));
|
|
||||||
translateX.value = newTranslateX;
|
|
||||||
})
|
|
||||||
.onEnd((event) => {
|
|
||||||
const shouldClose = translateX.value < -150 || event.velocityX < -500;
|
|
||||||
|
|
||||||
if (shouldClose) {
|
|
||||||
translateX.value = withTiming(-300, {
|
|
||||||
duration: 150,
|
|
||||||
easing: Easing.out(Easing.ease),
|
|
||||||
}, (finished) => {
|
|
||||||
if (finished) {
|
|
||||||
runOnJS(setIsVisible)(false);
|
|
||||||
runOnJS(onClose)();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
translateX.value = withTiming(0, {
|
|
||||||
duration: 150,
|
|
||||||
easing: Easing.out(Easing.ease),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const animatedSidebarStyle = useAnimatedStyle(() => ({
|
|
||||||
transform: [{ translateX: translateX.value }],
|
|
||||||
}));
|
|
||||||
|
|
||||||
const animatedBackdropStyle = useAnimatedStyle(() => {
|
|
||||||
// Map translateX (-300 to 0) to opacity (0 to 1)
|
|
||||||
const opacity = (translateX.value + 300) / 300;
|
|
||||||
return { opacity };
|
|
||||||
});
|
|
||||||
|
|
||||||
// Don't render if not visible
|
|
||||||
if (!isVisible) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{/* Backdrop - fade in/out with sidebar */}
|
|
||||||
<Pressable
|
|
||||||
onPress={onClose}
|
|
||||||
style={StyleSheet.absoluteFill}
|
|
||||||
pointerEvents={isOpen ? "auto" : "none"}
|
|
||||||
>
|
|
||||||
<Animated.View style={[styles.backdrop, animatedBackdropStyle]} />
|
|
||||||
</Pressable>
|
|
||||||
|
|
||||||
{/* Sidebar with close gesture */}
|
|
||||||
<GestureDetector gesture={closeGesture}>
|
|
||||||
<Animated.View
|
|
||||||
style={[
|
|
||||||
styles.sidebar,
|
|
||||||
{
|
|
||||||
paddingTop: insets.top + 16,
|
|
||||||
paddingBottom: insets.bottom + 16,
|
|
||||||
backgroundColor: theme.colors.background,
|
|
||||||
},
|
|
||||||
animatedSidebarStyle,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{/* Header */}
|
|
||||||
<View style={styles.header}>
|
|
||||||
<Text style={[styles.headerTitle, { color: theme.colors.foreground }]}>
|
|
||||||
Agents
|
|
||||||
</Text>
|
|
||||||
<Pressable onPress={onClose} style={styles.closeButton}>
|
|
||||||
<X size={24} color={theme.colors.mutedForeground} />
|
|
||||||
</Pressable>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* New Agent Button */}
|
|
||||||
<Pressable
|
|
||||||
style={[styles.newAgentButton, { backgroundColor: theme.colors.primary }]}
|
|
||||||
onPress={() => {
|
|
||||||
onNewAgent();
|
|
||||||
onClose();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Plus size={20} color={theme.colors.primaryForeground} />
|
|
||||||
<Text style={[styles.newAgentText, { color: theme.colors.primaryForeground }]}>
|
|
||||||
New Agent
|
|
||||||
</Text>
|
|
||||||
</Pressable>
|
|
||||||
|
|
||||||
{/* Agent List */}
|
|
||||||
<ScrollView style={styles.agentList} showsVerticalScrollIndicator={false}>
|
|
||||||
{agents.map((agent) => {
|
|
||||||
const isActive = agent.id === activeAgentId;
|
|
||||||
const statusColor = getAgentStatusColor(agent.status);
|
|
||||||
const statusLabel = getAgentStatusLabel(agent.status);
|
|
||||||
const providerLabel = getAgentProviderDefinition(agent.provider).label;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Pressable
|
|
||||||
key={agent.id}
|
|
||||||
style={[
|
|
||||||
styles.agentItem,
|
|
||||||
{ backgroundColor: isActive ? theme.colors.secondary : "transparent" },
|
|
||||||
]}
|
|
||||||
onPress={() => handleAgentSelect(agent.id, agent.serverId)}
|
|
||||||
>
|
|
||||||
<View style={styles.agentContent}>
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
styles.agentTitle,
|
|
||||||
{ color: theme.colors.foreground },
|
|
||||||
]}
|
|
||||||
numberOfLines={1}
|
|
||||||
>
|
|
||||||
{agent.title || "New Agent"}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<View style={styles.directoryRow}>
|
|
||||||
<Text
|
|
||||||
style={[styles.agentDirectory, { color: theme.colors.mutedForeground }]}
|
|
||||||
numberOfLines={1}
|
|
||||||
>
|
|
||||||
{agent.cwd}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<View style={styles.statusBadge}>
|
|
||||||
<View style={[styles.providerBadge, { backgroundColor: theme.colors.muted }]}>
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
styles.providerText,
|
|
||||||
{ color: theme.colors.mutedForeground },
|
|
||||||
]}
|
|
||||||
numberOfLines={1}
|
|
||||||
>
|
|
||||||
{providerLabel}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View
|
|
||||||
style={[styles.statusDot, { backgroundColor: statusColor }]}
|
|
||||||
/>
|
|
||||||
<Text style={[styles.statusText, { color: statusColor }]}>
|
|
||||||
{statusLabel}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</Pressable>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
{agents.length === 0 && (
|
|
||||||
<View style={styles.emptyState}>
|
|
||||||
<Text style={[styles.emptyText, { color: theme.colors.mutedForeground }]}>
|
|
||||||
No agents yet
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</ScrollView>
|
|
||||||
</Animated.View>
|
|
||||||
</GestureDetector>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
backdrop: {
|
|
||||||
flex: 1,
|
|
||||||
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
|
||||||
zIndex: 998,
|
|
||||||
},
|
|
||||||
sidebar: {
|
|
||||||
position: "absolute",
|
|
||||||
left: 0,
|
|
||||||
top: 0,
|
|
||||||
bottom: 0,
|
|
||||||
width: 300,
|
|
||||||
zIndex: 999,
|
|
||||||
paddingHorizontal: 16,
|
|
||||||
},
|
|
||||||
header: {
|
|
||||||
flexDirection: "row",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
alignItems: "center",
|
|
||||||
marginBottom: 16,
|
|
||||||
},
|
|
||||||
headerTitle: {
|
|
||||||
fontSize: 24,
|
|
||||||
fontWeight: "700",
|
|
||||||
},
|
|
||||||
closeButton: {
|
|
||||||
padding: 4,
|
|
||||||
},
|
|
||||||
newAgentButton: {
|
|
||||||
flexDirection: "row",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
paddingVertical: 12,
|
|
||||||
paddingHorizontal: 16,
|
|
||||||
borderRadius: 8,
|
|
||||||
marginBottom: 16,
|
|
||||||
gap: 8,
|
|
||||||
},
|
|
||||||
newAgentText: {
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: "600",
|
|
||||||
},
|
|
||||||
agentList: {
|
|
||||||
flex: 1,
|
|
||||||
},
|
|
||||||
agentItem: {
|
|
||||||
paddingVertical: 12,
|
|
||||||
paddingHorizontal: 12,
|
|
||||||
borderRadius: 8,
|
|
||||||
marginBottom: 4,
|
|
||||||
},
|
|
||||||
agentContent: {
|
|
||||||
flex: 1,
|
|
||||||
},
|
|
||||||
agentTitle: {
|
|
||||||
fontSize: 15,
|
|
||||||
fontWeight: "500",
|
|
||||||
marginBottom: 2,
|
|
||||||
},
|
|
||||||
directoryRow: {
|
|
||||||
flexDirection: "row",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: 8,
|
|
||||||
},
|
|
||||||
agentDirectory: {
|
|
||||||
flex: 1,
|
|
||||||
fontSize: 12,
|
|
||||||
},
|
|
||||||
statusBadge: {
|
|
||||||
flexDirection: "row",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: 4,
|
|
||||||
},
|
|
||||||
providerBadge: {
|
|
||||||
paddingHorizontal: 8,
|
|
||||||
paddingVertical: 2,
|
|
||||||
borderRadius: 999,
|
|
||||||
},
|
|
||||||
providerText: {
|
|
||||||
fontSize: 11,
|
|
||||||
fontWeight: "600",
|
|
||||||
},
|
|
||||||
statusDot: {
|
|
||||||
width: 6,
|
|
||||||
height: 6,
|
|
||||||
borderRadius: 3,
|
|
||||||
},
|
|
||||||
statusText: {
|
|
||||||
fontSize: 11,
|
|
||||||
fontWeight: "500",
|
|
||||||
},
|
|
||||||
emptyState: {
|
|
||||||
paddingVertical: 32,
|
|
||||||
alignItems: "center",
|
|
||||||
},
|
|
||||||
emptyText: {
|
|
||||||
fontSize: 14,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -19,8 +19,8 @@ export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Get the setAgentMode action (actions are stable, won't cause rerenders)
|
// Get the setAgentMode action (actions are stable, won't cause rerenders)
|
||||||
const setAgentMode = useSessionStore((state) =>
|
const setAgentMode = useSessionStore(
|
||||||
state.sessions[serverId]?.methods?.setAgentMode
|
(state) => state.sessions[serverId]?.methods?.setAgentMode
|
||||||
);
|
);
|
||||||
|
|
||||||
const [showModeSelector, setShowModeSelector] = useState(false);
|
const [showModeSelector, setShowModeSelector] = useState(false);
|
||||||
@@ -47,7 +47,8 @@ export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) {
|
|||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Text style={styles.modeBadgeText}>
|
<Text style={styles.modeBadgeText}>
|
||||||
{agent.availableModes?.find((m) => m.id === agent.currentModeId)?.label ||
|
{agent.availableModes?.find((m) => m.id === agent.currentModeId)
|
||||||
|
?.label ||
|
||||||
agent.currentModeId ||
|
agent.currentModeId ||
|
||||||
"default"}
|
"default"}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -75,14 +76,14 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
modeBadge: {
|
modeBadge: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: theme.spacing[2],
|
backgroundColor: theme.colors.popover,
|
||||||
|
gap: theme.spacing[1],
|
||||||
paddingHorizontal: theme.spacing[3],
|
paddingHorizontal: theme.spacing[3],
|
||||||
paddingVertical: theme.spacing[2],
|
paddingVertical: theme.spacing[2],
|
||||||
backgroundColor: theme.colors.muted,
|
borderRadius: theme.borderRadius["2xl"],
|
||||||
borderRadius: theme.borderRadius.full,
|
|
||||||
},
|
},
|
||||||
modeBadgePressed: {
|
modeBadgePressed: {
|
||||||
backgroundColor: theme.colors.accent,
|
backgroundColor: theme.colors.background,
|
||||||
},
|
},
|
||||||
modeBadgeText: {
|
modeBadgeText: {
|
||||||
color: theme.colors.mutedForeground,
|
color: theme.colors.mutedForeground,
|
||||||
|
|||||||
@@ -24,7 +24,9 @@ interface DictationControlsProps {
|
|||||||
function formatDuration(seconds: number): string {
|
function formatDuration(seconds: number): string {
|
||||||
const mins = Math.floor(seconds / 60);
|
const mins = Math.floor(seconds / 60);
|
||||||
const secs = seconds % 60;
|
const secs = seconds % 60;
|
||||||
return `${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`;
|
return `${mins.toString().padStart(2, "0")}:${secs
|
||||||
|
.toString()
|
||||||
|
.padStart(2, "0")}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DictationControls({
|
export function DictationControls({
|
||||||
@@ -122,7 +124,9 @@ export function DictationControls({
|
|||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
{retryStatusText && (isRetrying || isFailed) && (
|
{retryStatusText && (isRetrying || isFailed) && (
|
||||||
<Text style={[styles.statusLabel, { color: theme.colors.mutedForeground }]}>
|
<Text
|
||||||
|
style={[styles.statusLabel, { color: theme.colors.mutedForeground }]}
|
||||||
|
>
|
||||||
{retryStatusText}
|
{retryStatusText}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
@@ -158,7 +162,12 @@ export function DictationOverlay({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[overlayStyles.container, { backgroundColor: theme.colors.palette.blue[600] }]}>
|
<View
|
||||||
|
style={[
|
||||||
|
overlayStyles.container,
|
||||||
|
{ backgroundColor: theme.colors.palette.blue[600] },
|
||||||
|
]}
|
||||||
|
>
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={handleCancel}
|
onPress={handleCancel}
|
||||||
disabled={actionsDisabled && !isFailed}
|
disabled={actionsDisabled && !isFailed}
|
||||||
@@ -178,7 +187,12 @@ export function DictationOverlay({
|
|||||||
isSpeaking={false}
|
isSpeaking={false}
|
||||||
orientation="horizontal"
|
orientation="horizontal"
|
||||||
/>
|
/>
|
||||||
<Text style={[overlayStyles.timerText, { color: theme.colors.palette.white }]}>
|
<Text
|
||||||
|
style={[
|
||||||
|
overlayStyles.timerText,
|
||||||
|
{ color: theme.colors.palette.white },
|
||||||
|
]}
|
||||||
|
>
|
||||||
{formatDuration(duration)}
|
{formatDuration(duration)}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
@@ -186,28 +200,52 @@ export function DictationOverlay({
|
|||||||
<View style={overlayStyles.actionButtonsContainer}>
|
<View style={overlayStyles.actionButtonsContainer}>
|
||||||
{actionsDisabled ? (
|
{actionsDisabled ? (
|
||||||
<View style={overlayStyles.loadingContainer}>
|
<View style={overlayStyles.loadingContainer}>
|
||||||
<ActivityIndicator size="small" color={theme.colors.palette.white} />
|
<ActivityIndicator
|
||||||
|
size="small"
|
||||||
|
color={theme.colors.palette.white}
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
) : isFailed ? (
|
) : isFailed ? (
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={onRetry}
|
onPress={onRetry}
|
||||||
style={[overlayStyles.actionButton, { backgroundColor: theme.colors.palette.white }]}
|
style={[
|
||||||
|
overlayStyles.actionButton,
|
||||||
|
{ backgroundColor: theme.colors.palette.white },
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
<RefreshCcw size={20} color={theme.colors.palette.blue[600]} strokeWidth={2.5} />
|
<RefreshCcw
|
||||||
|
size={20}
|
||||||
|
color={theme.colors.palette.blue[600]}
|
||||||
|
strokeWidth={2.5}
|
||||||
|
/>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={onAccept}
|
onPress={onAccept}
|
||||||
style={[overlayStyles.actionButton, { backgroundColor: "rgba(255, 255, 255, 0.25)" }]}
|
style={[
|
||||||
|
overlayStyles.actionButton,
|
||||||
|
{ backgroundColor: "rgba(255, 255, 255, 0.25)" },
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
<Check size={20} color={theme.colors.palette.white} strokeWidth={2.5} />
|
<Check
|
||||||
|
size={20}
|
||||||
|
color={theme.colors.palette.white}
|
||||||
|
strokeWidth={2.5}
|
||||||
|
/>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={onAcceptAndSend}
|
onPress={onAcceptAndSend}
|
||||||
style={[overlayStyles.actionButton, { backgroundColor: theme.colors.palette.white }]}
|
style={[
|
||||||
|
overlayStyles.actionButton,
|
||||||
|
{ backgroundColor: theme.colors.palette.white },
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
<ArrowUp size={20} color={theme.colors.palette.blue[600]} strokeWidth={2.5} />
|
<ArrowUp
|
||||||
|
size={20}
|
||||||
|
color={theme.colors.palette.blue[600]}
|
||||||
|
strokeWidth={2.5}
|
||||||
|
/>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -290,6 +328,8 @@ const overlayStyles = StyleSheet.create((theme) => ({
|
|||||||
container: {
|
container: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
|
width: "100%",
|
||||||
|
borderRadius: theme.borderRadius["2xl"],
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
paddingHorizontal: theme.spacing[4],
|
paddingHorizontal: theme.spacing[4],
|
||||||
paddingVertical: OVERLAY_VERTICAL_PADDING,
|
paddingVertical: OVERLAY_VERTICAL_PADDING,
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export function BackHeader({ title, rightContent, onBack }: BackHeaderProps) {
|
|||||||
onPress={onBack ?? (() => router.back())}
|
onPress={onBack ?? (() => router.back())}
|
||||||
style={styles.backButton}
|
style={styles.backButton}
|
||||||
>
|
>
|
||||||
<ArrowLeft size={20} color={theme.colors.foreground} />
|
<ArrowLeft size={20} color={theme.colors.mutedForeground} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
{title && (
|
{title && (
|
||||||
<Text style={styles.title} numberOfLines={1}>
|
<Text style={styles.title} numberOfLines={1}>
|
||||||
@@ -39,16 +39,22 @@ export function BackHeader({ title, rightContent, onBack }: BackHeaderProps) {
|
|||||||
|
|
||||||
const styles = StyleSheet.create((theme) => ({
|
const styles = StyleSheet.create((theme) => ({
|
||||||
left: {
|
left: {
|
||||||
gap: theme.spacing[3],
|
gap: theme.spacing[2],
|
||||||
},
|
},
|
||||||
backButton: {
|
backButton: {
|
||||||
padding: theme.spacing[3],
|
padding: {
|
||||||
|
xs: theme.spacing[3],
|
||||||
|
md: theme.spacing[2],
|
||||||
|
},
|
||||||
borderRadius: theme.borderRadius.lg,
|
borderRadius: theme.borderRadius.lg,
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
fontSize: theme.fontSize.lg,
|
fontSize: theme.fontSize.lg,
|
||||||
fontWeight: theme.fontWeight.semibold,
|
fontWeight: {
|
||||||
|
xs: theme.fontWeight.semibold,
|
||||||
|
md: "400",
|
||||||
|
},
|
||||||
color: theme.colors.foreground,
|
color: theme.colors.foreground,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -51,7 +51,10 @@ export function HomeHeader({ onCreateAgent, onImportAgent }: HomeHeaderProps) {
|
|||||||
|
|
||||||
const styles = StyleSheet.create((theme) => ({
|
const styles = StyleSheet.create((theme) => ({
|
||||||
iconButton: {
|
iconButton: {
|
||||||
padding: theme.spacing[3],
|
padding: {
|
||||||
|
xs: theme.spacing[3],
|
||||||
|
md: theme.spacing[2],
|
||||||
|
},
|
||||||
borderRadius: theme.borderRadius.lg,
|
borderRadius: theme.borderRadius.lg,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { View, type StyleProp, type ViewStyle } from "react-native";
|
import { View, type StyleProp, type ViewStyle } from "react-native";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
|
||||||
|
|
||||||
interface ScreenHeaderProps {
|
interface ScreenHeaderProps {
|
||||||
left?: ReactNode;
|
left?: ReactNode;
|
||||||
@@ -17,10 +17,11 @@ interface ScreenHeaderProps {
|
|||||||
export function ScreenHeader({ left, right, leftStyle, rightStyle }: ScreenHeaderProps) {
|
export function ScreenHeader({ left, right, leftStyle, rightStyle }: ScreenHeaderProps) {
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
|
const topPadding = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm" ? 8 : 4;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<View style={[styles.inner, { paddingTop: insets.top + 12 }]}>
|
<View style={[styles.inner, { paddingTop: insets.top + topPadding }]}>
|
||||||
<View style={styles.row}>
|
<View style={styles.row}>
|
||||||
<View style={[styles.left, leftStyle]}>{left}</View>
|
<View style={[styles.left, leftStyle]}>{left}</View>
|
||||||
<View style={[styles.right, rightStyle]}>{right}</View>
|
<View style={[styles.right, rightStyle]}>{right}</View>
|
||||||
@@ -39,8 +40,14 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
paddingHorizontal: theme.spacing[3],
|
paddingHorizontal: {
|
||||||
paddingBottom: theme.spacing[2],
|
xs: theme.spacing[2],
|
||||||
|
md: theme.spacing[1],
|
||||||
|
},
|
||||||
|
paddingBottom: {
|
||||||
|
xs: theme.spacing[2],
|
||||||
|
md: theme.spacing[1],
|
||||||
|
},
|
||||||
borderBottomWidth: theme.borderWidth[1],
|
borderBottomWidth: theme.borderWidth[1],
|
||||||
borderBottomColor: theme.colors.border,
|
borderBottomColor: theme.colors.border,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import Svg, { Path } from "react-native-svg";
|
||||||
|
|
||||||
|
interface ClaudeIconProps {
|
||||||
|
size?: number;
|
||||||
|
color?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ClaudeIcon({ size = 16, color = "currentColor" }: ClaudeIconProps) {
|
||||||
|
return (
|
||||||
|
<Svg width={size} height={size} viewBox="0 0 24 24" fill={color} fillRule="evenodd">
|
||||||
|
<Path d="M4.709 15.955l4.72-2.647.08-.23-.08-.128H9.2l-.79-.048-2.698-.073-2.339-.097-2.266-.122-.571-.121L0 11.784l.055-.352.48-.321.686.06 1.52.103 2.278.158 1.652.097 2.449.255h.389l.055-.157-.134-.098-.103-.097-2.358-1.596-2.552-1.688-1.336-.972-.724-.491-.364-.462-.158-1.008.656-.722.881.06.225.061.893.686 1.908 1.476 2.491 1.833.365.304.145-.103.019-.073-.164-.274-1.355-2.446-1.446-2.49-.644-1.032-.17-.619a2.97 2.97 0 01-.104-.729L6.283.134 6.696 0l.996.134.42.364.62 1.414 1.002 2.229 1.555 3.03.456.898.243.832.091.255h.158V9.01l.128-1.706.237-2.095.23-2.695.08-.76.376-.91.747-.492.584.28.48.685-.067.444-.286 1.851-.559 2.903-.364 1.942h.212l.243-.242.985-1.306 1.652-2.064.73-.82.85-.904.547-.431h1.033l.76 1.129-.34 1.166-1.064 1.347-.881 1.142-1.264 1.7-.79 1.36.073.11.188-.02 2.856-.606 1.543-.28 1.841-.315.833.388.091.395-.328.807-1.969.486-2.309.462-3.439.813-.042.03.049.061 1.549.146.662.036h1.622l3.02.225.79.522.474.638-.079.485-1.215.62-1.64-.389-3.829-.91-1.312-.329h-.182v.11l1.093 1.068 2.006 1.81 2.509 2.33.127.578-.322.455-.34-.049-2.205-1.657-.851-.747-1.926-1.62h-.128v.17l.444.649 2.345 3.521.122 1.08-.17.353-.608.213-.668-.122-1.374-1.925-1.415-2.167-1.143-1.943-.14.08-.674 7.254-.316.37-.729.28-.607-.461-.322-.747.322-1.476.389-1.924.315-1.53.286-1.9.17-.632-.012-.042-.14.018-1.434 1.967-2.18 2.945-1.726 1.845-.414.164-.717-.37.067-.662.401-.589 2.388-3.036 1.44-1.882.93-1.086-.006-.158h-.055L4.132 18.56l-1.13.146-.487-.456.061-.746.231-.243 1.908-1.312-.006.006z" />
|
||||||
|
</Svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import Svg, { Path } from "react-native-svg";
|
||||||
|
|
||||||
|
interface CodexIconProps {
|
||||||
|
size?: number;
|
||||||
|
color?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CodexIcon({ size = 16, color = "currentColor" }: CodexIconProps) {
|
||||||
|
return (
|
||||||
|
<Svg width={size} height={size} viewBox="0 0 24 24" fill={color} fillRule="evenodd">
|
||||||
|
<Path d="M21.55 10.004a5.416 5.416 0 00-.478-4.501c-1.217-2.09-3.662-3.166-6.05-2.66A5.59 5.59 0 0010.831 1C8.39.995 6.224 2.546 5.473 4.838A5.553 5.553 0 001.76 7.496a5.487 5.487 0 00.691 6.5 5.416 5.416 0 00.477 4.502c1.217 2.09 3.662 3.165 6.05 2.66A5.586 5.586 0 0013.168 23c2.443.006 4.61-1.546 5.361-3.84a5.553 5.553 0 003.715-2.66 5.488 5.488 0 00-.693-6.497v.001zm-8.381 11.558a4.199 4.199 0 01-2.675-.954c.034-.018.093-.05.132-.074l4.44-2.53a.71.71 0 00.364-.623v-6.176l1.877 1.069c.02.01.033.029.036.05v5.115c-.003 2.274-1.87 4.118-4.174 4.123zM4.192 17.78a4.059 4.059 0 01-.498-2.763c.032.02.09.055.131.078l4.44 2.53c.225.13.504.13.73 0l5.42-3.088v2.138a.068.068 0 01-.027.057L9.9 19.288c-1.999 1.136-4.552.46-5.707-1.51h-.001zM3.023 8.216A4.15 4.15 0 015.198 6.41l-.002.151v5.06a.711.711 0 00.364.624l5.42 3.087-1.876 1.07a.067.067 0 01-.063.005l-4.489-2.559c-1.995-1.14-2.679-3.658-1.53-5.63h.001zm15.417 3.54l-5.42-3.088L14.896 7.6a.067.067 0 01.063-.006l4.489 2.557c1.998 1.14 2.683 3.662 1.529 5.633a4.163 4.163 0 01-2.174 1.807V12.38a.71.71 0 00-.363-.623zm1.867-2.773a6.04 6.04 0 00-.132-.078l-4.44-2.53a.731.731 0 00-.729 0l-5.42 3.088V7.325a.068.068 0 01.027-.057L14.1 4.713c2-1.137 4.555-.46 5.707 1.513.487.833.664 1.809.499 2.757h.001zm-11.741 3.81l-1.877-1.068a.065.065 0 01-.036-.051V6.559c.001-2.277 1.873-4.122 4.181-4.12.976 0 1.92.338 2.671.954-.034.018-.092.05-.131.073l-4.44 2.53a.71.71 0 00-.365.623l-.003 6.173v.002zm1.02-2.168L12 9.25l2.414 1.375v2.75L12 14.75l-2.415-1.375v-2.75z" />
|
||||||
|
</Svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
TextInputKeyPressEventData,
|
TextInputKeyPressEventData,
|
||||||
Image,
|
Image,
|
||||||
Platform,
|
Platform,
|
||||||
|
BackHandler,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { useState, useRef, useCallback, useEffect } from "react";
|
import { useState, useRef, useCallback, useEffect } from "react";
|
||||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
@@ -57,7 +58,7 @@ export interface MessageInputProps {
|
|||||||
onQueue?: (payload: MessagePayload) => void;
|
onQueue?: (payload: MessagePayload) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MIN_INPUT_HEIGHT = 50;
|
const MIN_INPUT_HEIGHT = 30;
|
||||||
const MAX_INPUT_HEIGHT = 160;
|
const MAX_INPUT_HEIGHT = 160;
|
||||||
const IS_WEB = Platform.OS === "web";
|
const IS_WEB = Platform.OS === "web";
|
||||||
|
|
||||||
@@ -98,7 +99,9 @@ export function MessageInput({
|
|||||||
}: MessageInputProps) {
|
}: MessageInputProps) {
|
||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
const [inputHeight, setInputHeight] = useState(MIN_INPUT_HEIGHT);
|
const [inputHeight, setInputHeight] = useState(MIN_INPUT_HEIGHT);
|
||||||
const textInputRef = useRef<TextInput | (TextInput & { getNativeRef?: () => unknown }) | null>(null);
|
const textInputRef = useRef<
|
||||||
|
TextInput | (TextInput & { getNativeRef?: () => unknown }) | null
|
||||||
|
>(null);
|
||||||
const inputHeightRef = useRef(MIN_INPUT_HEIGHT);
|
const inputHeightRef = useRef(MIN_INPUT_HEIGHT);
|
||||||
const baselineInputHeightRef = useRef<number | null>(null);
|
const baselineInputHeightRef = useRef<number | null>(null);
|
||||||
const overlayTransition = useSharedValue(0);
|
const overlayTransition = useSharedValue(0);
|
||||||
@@ -188,8 +191,14 @@ export function MessageInput({
|
|||||||
|
|
||||||
// Animate overlay
|
// Animate overlay
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const showOverlay = isDictating || isDictationProcessing || dictationStatus === "retrying" || dictationStatus === "failed";
|
const showOverlay =
|
||||||
overlayTransition.value = withTiming(showOverlay ? 1 : 0, { duration: 200 });
|
isDictating ||
|
||||||
|
isDictationProcessing ||
|
||||||
|
dictationStatus === "retrying" ||
|
||||||
|
dictationStatus === "failed";
|
||||||
|
overlayTransition.value = withTiming(showOverlay ? 1 : 0, {
|
||||||
|
duration: 200,
|
||||||
|
});
|
||||||
}, [isDictating, isDictationProcessing, dictationStatus, overlayTransition]);
|
}, [isDictating, isDictationProcessing, dictationStatus, overlayTransition]);
|
||||||
|
|
||||||
const overlayAnimatedStyle = useAnimatedStyle(() => ({
|
const overlayAnimatedStyle = useAnimatedStyle(() => ({
|
||||||
@@ -234,7 +243,10 @@ export function MessageInput({
|
|||||||
const handleSendMessage = useCallback(() => {
|
const handleSendMessage = useCallback(() => {
|
||||||
const trimmed = value.trim();
|
const trimmed = value.trim();
|
||||||
if (!trimmed && images.length === 0) return;
|
if (!trimmed && images.length === 0) return;
|
||||||
const payload = { text: trimmed, images: images.length > 0 ? images : undefined };
|
const payload = {
|
||||||
|
text: trimmed,
|
||||||
|
images: images.length > 0 ? images : undefined,
|
||||||
|
};
|
||||||
if (isAgentRunning && onQueue) {
|
if (isAgentRunning && onQueue) {
|
||||||
onQueue(payload);
|
onQueue(payload);
|
||||||
} else {
|
} else {
|
||||||
@@ -286,7 +298,10 @@ export function MessageInput({
|
|||||||
|
|
||||||
const baseline = baselineInputHeightRef.current ?? MIN_INPUT_HEIGHT;
|
const baseline = baselineInputHeightRef.current ?? MIN_INPUT_HEIGHT;
|
||||||
const rawTarget = scrollHeight > 0 ? scrollHeight : baseline;
|
const rawTarget = scrollHeight > 0 ? scrollHeight : baseline;
|
||||||
const bounded = Math.max(MIN_INPUT_HEIGHT, Math.min(MAX_INPUT_HEIGHT, rawTarget));
|
const bounded = Math.max(
|
||||||
|
MIN_INPUT_HEIGHT,
|
||||||
|
Math.min(MAX_INPUT_HEIGHT, rawTarget)
|
||||||
|
);
|
||||||
|
|
||||||
if (Math.abs(inputHeightRef.current - bounded) >= 1) {
|
if (Math.abs(inputHeightRef.current - bounded) >= 1) {
|
||||||
inputHeightRef.current = bounded;
|
inputHeightRef.current = bounded;
|
||||||
@@ -297,7 +312,10 @@ export function MessageInput({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setBoundedInputHeight(nextHeight: number) {
|
function setBoundedInputHeight(nextHeight: number) {
|
||||||
const bounded = Math.max(MIN_INPUT_HEIGHT, Math.min(MAX_INPUT_HEIGHT, nextHeight));
|
const bounded = Math.max(
|
||||||
|
MIN_INPUT_HEIGHT,
|
||||||
|
Math.min(MAX_INPUT_HEIGHT, nextHeight)
|
||||||
|
);
|
||||||
if (Math.abs(inputHeightRef.current - bounded) < 1) return;
|
if (Math.abs(inputHeightRef.current - bounded) < 1) return;
|
||||||
inputHeightRef.current = bounded;
|
inputHeightRef.current = bounded;
|
||||||
setInputHeight(bounded);
|
setInputHeight(bounded);
|
||||||
@@ -364,9 +382,15 @@ export function MessageInput({
|
|||||||
<View style={styles.imagePreviewContainer}>
|
<View style={styles.imagePreviewContainer}>
|
||||||
{images.map((image, index) => (
|
{images.map((image, index) => (
|
||||||
<View key={`${image.uri}-${index}`} style={styles.imagePill}>
|
<View key={`${image.uri}-${index}`} style={styles.imagePill}>
|
||||||
<Image source={{ uri: image.uri }} style={styles.imageThumbnail} />
|
<Image
|
||||||
|
source={{ uri: image.uri }}
|
||||||
|
style={styles.imageThumbnail}
|
||||||
|
/>
|
||||||
{onRemoveImage && (
|
{onRemoveImage && (
|
||||||
<Pressable onPress={() => onRemoveImage(index)} style={styles.removeImageButton}>
|
<Pressable
|
||||||
|
onPress={() => onRemoveImage(index)}
|
||||||
|
style={styles.removeImageButton}
|
||||||
|
>
|
||||||
<X size={16} color={theme.colors.foreground} />
|
<X size={16} color={theme.colors.foreground} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
)}
|
)}
|
||||||
@@ -384,14 +408,20 @@ export function MessageInput({
|
|||||||
placeholderTextColor={theme.colors.mutedForeground}
|
placeholderTextColor={theme.colors.mutedForeground}
|
||||||
style={[
|
style={[
|
||||||
styles.textInput,
|
styles.textInput,
|
||||||
{ height: inputHeight, minHeight: MIN_INPUT_HEIGHT, maxHeight: MAX_INPUT_HEIGHT },
|
{
|
||||||
|
height: inputHeight,
|
||||||
|
minHeight: MIN_INPUT_HEIGHT,
|
||||||
|
maxHeight: MAX_INPUT_HEIGHT,
|
||||||
|
},
|
||||||
]}
|
]}
|
||||||
multiline
|
multiline
|
||||||
scrollEnabled={inputHeight >= MAX_INPUT_HEIGHT}
|
scrollEnabled={inputHeight >= MAX_INPUT_HEIGHT}
|
||||||
onContentSizeChange={handleContentSizeChange}
|
onContentSizeChange={handleContentSizeChange}
|
||||||
editable={!isDictating && isConnected && !disabled}
|
editable={!isDictating && isConnected && !disabled}
|
||||||
onKeyPress={shouldHandleDesktopSubmit ? handleDesktopSubmitKeyPress : undefined}
|
onKeyPress={
|
||||||
autoFocus={autoFocus}
|
shouldHandleDesktopSubmit ? handleDesktopSubmitKeyPress : undefined
|
||||||
|
}
|
||||||
|
autoFocus={IS_WEB && autoFocus}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Button row */}
|
{/* Button row */}
|
||||||
@@ -418,10 +448,19 @@ export function MessageInput({
|
|||||||
{shouldShowSendButton && (
|
{shouldShowSendButton && (
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={handleSendMessage}
|
onPress={handleSendMessage}
|
||||||
disabled={!isConnected || isSubmitDisabled || isSubmitLoading || disabled}
|
disabled={
|
||||||
|
!isConnected ||
|
||||||
|
isSubmitDisabled ||
|
||||||
|
isSubmitLoading ||
|
||||||
|
disabled
|
||||||
|
}
|
||||||
style={[
|
style={[
|
||||||
styles.sendButton,
|
styles.sendButton,
|
||||||
(!isConnected || isSubmitDisabled || isSubmitLoading || disabled) && styles.buttonDisabled,
|
(!isConnected ||
|
||||||
|
isSubmitDisabled ||
|
||||||
|
isSubmitLoading ||
|
||||||
|
disabled) &&
|
||||||
|
styles.buttonDisabled,
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<ArrowUp size={20} color="white" />
|
<ArrowUp size={20} color="white" />
|
||||||
@@ -458,8 +497,16 @@ export function MessageInput({
|
|||||||
onCancel={handleCancelRecording}
|
onCancel={handleCancelRecording}
|
||||||
onAccept={handleAcceptRecording}
|
onAccept={handleAcceptRecording}
|
||||||
onAcceptAndSend={handleAcceptAndSendRecording}
|
onAcceptAndSend={handleAcceptAndSendRecording}
|
||||||
onRetry={dictationStatus === "failed" ? handleRetryFailedRecording : undefined}
|
onRetry={
|
||||||
onDiscard={dictationStatus === "failed" ? handleDiscardFailedRecording : undefined}
|
dictationStatus === "failed"
|
||||||
|
? handleRetryFailedRecording
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
onDiscard={
|
||||||
|
dictationStatus === "failed"
|
||||||
|
? handleDiscardFailedRecording
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
</View>
|
</View>
|
||||||
@@ -473,6 +520,18 @@ const styles = StyleSheet.create(((theme: any) => ({
|
|||||||
inputWrapper: {
|
inputWrapper: {
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
gap: theme.spacing[3],
|
gap: theme.spacing[3],
|
||||||
|
backgroundColor: theme.colors.muted,
|
||||||
|
borderWidth: theme.borderWidth[1],
|
||||||
|
borderColor: theme.colors.accentBorder,
|
||||||
|
borderRadius: theme.borderRadius["2xl"],
|
||||||
|
paddingVertical: {
|
||||||
|
xs: theme.spacing[2],
|
||||||
|
md: theme.spacing[4],
|
||||||
|
},
|
||||||
|
paddingHorizontal: {
|
||||||
|
xs: theme.spacing[3],
|
||||||
|
md: theme.spacing[4],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
imagePreviewContainer: {
|
imagePreviewContainer: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
@@ -500,12 +559,9 @@ const styles = StyleSheet.create(((theme: any) => ({
|
|||||||
},
|
},
|
||||||
textInput: {
|
textInput: {
|
||||||
width: "100%",
|
width: "100%",
|
||||||
paddingHorizontal: theme.spacing[4],
|
|
||||||
paddingVertical: theme.spacing[3],
|
|
||||||
backgroundColor: theme.colors.muted,
|
|
||||||
borderRadius: theme.borderRadius.lg,
|
|
||||||
color: theme.colors.foreground,
|
color: theme.colors.foreground,
|
||||||
fontSize: theme.fontSize.lg,
|
fontSize: theme.fontSize.lg,
|
||||||
|
fontWeight: theme.fontWeight.normal,
|
||||||
lineHeight: theme.fontSize.lg * 1.4,
|
lineHeight: theme.fontSize.lg * 1.4,
|
||||||
...(IS_WEB
|
...(IS_WEB
|
||||||
? {
|
? {
|
||||||
@@ -531,16 +587,15 @@ const styles = StyleSheet.create(((theme: any) => ({
|
|||||||
gap: theme.spacing[2],
|
gap: theme.spacing[2],
|
||||||
},
|
},
|
||||||
attachButton: {
|
attachButton: {
|
||||||
width: 40,
|
width: 20,
|
||||||
height: 40,
|
height: 20,
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
},
|
},
|
||||||
voiceButton: {
|
voiceButton: {
|
||||||
width: 40,
|
width: 34,
|
||||||
height: 40,
|
height: 34,
|
||||||
borderRadius: theme.borderRadius.full,
|
borderRadius: theme.borderRadius.full,
|
||||||
backgroundColor: "transparent",
|
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
},
|
},
|
||||||
@@ -548,8 +603,8 @@ const styles = StyleSheet.create(((theme: any) => ({
|
|||||||
backgroundColor: theme.colors.destructive,
|
backgroundColor: theme.colors.destructive,
|
||||||
},
|
},
|
||||||
sendButton: {
|
sendButton: {
|
||||||
width: 40,
|
width: 34,
|
||||||
height: 40,
|
height: 34,
|
||||||
borderRadius: theme.borderRadius.full,
|
borderRadius: theme.borderRadius.full,
|
||||||
backgroundColor: theme.colors.palette.blue[600],
|
backgroundColor: theme.colors.palette.blue[600],
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
@@ -560,8 +615,13 @@ const styles = StyleSheet.create(((theme: any) => ({
|
|||||||
},
|
},
|
||||||
overlayContainer: {
|
overlayContainer: {
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
right: 0,
|
right: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -14,7 +14,9 @@ export const baseColors = {
|
|||||||
600: "#52525b",
|
600: "#52525b",
|
||||||
700: "#3f3f46",
|
700: "#3f3f46",
|
||||||
800: "#27272a",
|
800: "#27272a",
|
||||||
|
850: "#1a1a1d",
|
||||||
900: "#18181b",
|
900: "#18181b",
|
||||||
|
950: "#121214",
|
||||||
},
|
},
|
||||||
|
|
||||||
// Gray scale
|
// Gray scale
|
||||||
@@ -120,6 +122,7 @@ const lightSemanticColors = {
|
|||||||
destructive: "#ef4444",
|
destructive: "#ef4444",
|
||||||
destructiveForeground: "#fafafa",
|
destructiveForeground: "#fafafa",
|
||||||
border: "#e4e4e7",
|
border: "#e4e4e7",
|
||||||
|
accentBorder: "#3f3f46",
|
||||||
input: "#e4e4e7",
|
input: "#e4e4e7",
|
||||||
ring: "#18181b",
|
ring: "#18181b",
|
||||||
} as const;
|
} as const;
|
||||||
@@ -141,7 +144,8 @@ const darkSemanticColors = {
|
|||||||
accentForeground: "#fafafa",
|
accentForeground: "#fafafa",
|
||||||
destructive: "#7f1d1d",
|
destructive: "#7f1d1d",
|
||||||
destructiveForeground: "#fafafa",
|
destructiveForeground: "#fafafa",
|
||||||
border: "#27272a",
|
border: "#27272e",
|
||||||
|
accentBorder: "#414146",
|
||||||
input: "#27272a",
|
input: "#27272a",
|
||||||
ring: "#d4d4d8",
|
ring: "#d4d4d8",
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
Reference in New Issue
Block a user