mirror of https://github.com/kortix-ai/suna.git
mobile responsive v1
This commit is contained in:
parent
6e5af01120
commit
b525c56c26
|
@ -3,9 +3,14 @@
|
|||
import React, { useState, Suspense, useEffect } from 'react';
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Menu } from "lucide-react";
|
||||
import { ChatInput } from '@/components/thread/chat-input';
|
||||
import { createProject, addUserMessage, startAgent, createThread } from "@/lib/api";
|
||||
import { generateThreadName } from "@/lib/actions/threads";
|
||||
import { useIsMobile } from "@/hooks/use-mobile";
|
||||
import { useSidebar } from "@/components/ui/sidebar";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
|
||||
// Constant for localStorage key to ensure consistency
|
||||
const PENDING_PROMPT_KEY = 'pendingAgentPrompt';
|
||||
|
@ -15,6 +20,8 @@ function DashboardContent() {
|
|||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [autoSubmit, setAutoSubmit] = useState(false);
|
||||
const router = useRouter();
|
||||
const isMobile = useIsMobile();
|
||||
const { setOpenMobile } = useSidebar();
|
||||
|
||||
const handleSubmit = async (message: string, options?: { model_name?: string; enable_thinking?: boolean }) => {
|
||||
if (!message.trim() || isSubmitting) return;
|
||||
|
@ -84,6 +91,25 @@ function DashboardContent() {
|
|||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-full w-full">
|
||||
{isMobile && (
|
||||
<div className="absolute top-4 left-4 z-10">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8"
|
||||
onClick={() => setOpenMobile(true)}
|
||||
>
|
||||
<Menu className="h-4 w-4" />
|
||||
<span className="sr-only">Open menu</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Open menu</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-[560px] max-w-[90%]">
|
||||
<div className="text-center mb-10">
|
||||
<h1 className="text-4xl font-medium text-foreground mb-2">Hey </h1>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
"use client"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { FolderOpen, Link, PanelRightOpen, Check, X } from "lucide-react"
|
||||
import { FolderOpen, Link, PanelRightOpen, Check, X, Menu } from "lucide-react"
|
||||
import { usePathname } from "next/navigation"
|
||||
import { toast } from "sonner"
|
||||
import {
|
||||
|
@ -16,6 +16,7 @@ import { updateProject } from "@/lib/api"
|
|||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import { useIsMobile } from "@/hooks/use-mobile"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useSidebar } from "@/components/ui/sidebar"
|
||||
|
||||
interface ThreadSiteHeaderProps {
|
||||
threadId: string
|
||||
|
@ -41,6 +42,7 @@ export function SiteHeader({
|
|||
const [editName, setEditName] = useState(projectName)
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
const isMobile = useIsMobile() || isMobileView
|
||||
const { setOpenMobile } = useSidebar()
|
||||
|
||||
const copyCurrentUrl = () => {
|
||||
const url = window.location.origin + pathname
|
||||
|
@ -106,9 +108,21 @@ export function SiteHeader({
|
|||
|
||||
return (
|
||||
<header className={cn(
|
||||
"bg-background sticky top-0 flex h-14 shrink-0 items-center gap-2 z-20 border-b w-full",
|
||||
"bg-background sticky top-0 flex h-14 shrink-0 items-center gap-2 z-20 w-full",
|
||||
isMobile && "px-2"
|
||||
)}>
|
||||
{isMobile && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setOpenMobile(true)}
|
||||
className="h-9 w-9 mr-1"
|
||||
aria-label="Open sidebar"
|
||||
>
|
||||
<Menu className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<div className="flex flex-1 items-center gap-2 px-3">
|
||||
{isEditing ? (
|
||||
<div className="flex items-center gap-1">
|
||||
|
|
|
@ -5,7 +5,7 @@ import { getToolIcon } from "@/components/thread/utils";
|
|||
import React from "react";
|
||||
import { Slider } from "@/components/ui/slider";
|
||||
import { ApiMessageType } from '@/components/thread/types';
|
||||
import { CircleDashed, X } from "lucide-react";
|
||||
import { CircleDashed, X, ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useIsMobile } from "@/hooks/use-mobile";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
@ -267,14 +267,48 @@ export function ToolCallSidePanel({
|
|||
return () => clearInterval(interval);
|
||||
}, [isStreaming]);
|
||||
|
||||
// Handle navigation with safety checks
|
||||
const navigateToPrevious = React.useCallback(() => {
|
||||
if (currentIndex > 0) {
|
||||
onNavigate(currentIndex - 1);
|
||||
}
|
||||
}, [currentIndex, onNavigate]);
|
||||
|
||||
const navigateToNext = React.useCallback(() => {
|
||||
if (currentIndex < totalCalls - 1) {
|
||||
onNavigate(currentIndex + 1);
|
||||
}
|
||||
}, [currentIndex, totalCalls, onNavigate]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const renderContent = () => {
|
||||
if (!currentToolCall) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full p-4">
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Always show header with close button for empty state */}
|
||||
<div className="pt-4 pl-4 pr-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<h2 className="text-lg font-medium text-zinc-900 dark:text-zinc-100">Suna's Computer</h2>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={onClose}
|
||||
className="h-8 w-8"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Empty state message */}
|
||||
<div className="flex items-center justify-center flex-1 p-4">
|
||||
<p className="text-sm text-zinc-500 dark:text-zinc-400 text-center">No tool call details available.</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -293,7 +327,34 @@ export function ToolCallSidePanel({
|
|||
isStreaming
|
||||
);
|
||||
|
||||
if (!toolView) return null;
|
||||
if (!toolView) {
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Header with close button even when no tool view */}
|
||||
<div className="pt-4 pl-4 pr-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<h2 className="text-lg font-medium text-zinc-900 dark:text-zinc-100">Suna's Computer</h2>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={onClose}
|
||||
className="h-8 w-8"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Error state message */}
|
||||
<div className="flex items-center justify-center flex-1 p-4">
|
||||
<p className="text-sm text-zinc-500 dark:text-zinc-400 text-center">Unable to display tool details.</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
|
@ -303,17 +364,6 @@ export function ToolCallSidePanel({
|
|||
<h2 className="text-lg font-medium text-zinc-900 dark:text-zinc-100">Suna's Computer</h2>
|
||||
</div>
|
||||
|
||||
{isMobile && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={onClose}
|
||||
className="h-8 w-8 -mr-2"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{currentToolCall.toolResult?.content && !isStreaming && (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-6 w-6 rounded-full bg-zinc-100 dark:bg-zinc-800 flex items-center justify-center">
|
||||
|
@ -333,17 +383,53 @@ export function ToolCallSidePanel({
|
|||
)}>
|
||||
{isSuccess ? 'Success' : 'Failed'}
|
||||
</div>
|
||||
|
||||
{/* Add close button */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={onClose}
|
||||
className="h-8 w-8 ml-1"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isStreaming && (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-50 text-blue-700 dark:bg-blue-900/20 dark:text-blue-400 flex items-center gap-1.5">
|
||||
<CircleDashed className="h-3 w-3 animate-spin" />
|
||||
<span>Running</span>
|
||||
</div>
|
||||
|
||||
{/* Add close button */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={onClose}
|
||||
className="h-8 w-8 ml-1"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Show close button when no status is available */}
|
||||
{!currentToolCall.toolResult?.content && !isStreaming && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={onClose}
|
||||
className="h-8 w-8"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content area */}
|
||||
<div className="flex-1 overflow-auto scrollbar-thin scrollbar-thumb-zinc-300 dark:scrollbar-thumb-zinc-700 scrollbar-track-transparent">
|
||||
{toolView}
|
||||
</div>
|
||||
|
@ -353,17 +439,23 @@ export function ToolCallSidePanel({
|
|||
|
||||
return (
|
||||
<div className={cn(
|
||||
"fixed inset-y-0 right-0 border-l flex flex-col z-10 h-screen transition-all duration-200 ease-in-out",
|
||||
"fixed inset-y-0 right-0 border-l flex flex-col z-30 h-screen transition-all duration-200 ease-in-out",
|
||||
isMobile
|
||||
? "w-[90%]"
|
||||
? "w-full"
|
||||
: "w-[90%] sm:w-[450px] md:w-[500px] lg:w-[550px] xl:w-[650px]",
|
||||
!isOpen && "translate-x-full"
|
||||
)}>
|
||||
<div className="flex-1 flex flex-col overflow-hidden">
|
||||
<div className="flex-1 flex flex-col overflow-hidden bg-background">
|
||||
{renderContent()}
|
||||
</div>
|
||||
|
||||
{/* Navigation controls */}
|
||||
{totalCalls > 1 && (
|
||||
<div className="p-4 border-t border-zinc-200 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-900 space-y-2">
|
||||
<div className={cn(
|
||||
"border-t border-zinc-200 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-900",
|
||||
isMobile ? "p-3" : "p-4 space-y-2"
|
||||
)}>
|
||||
{!isMobile && (
|
||||
<div className="flex justify-between items-center gap-4">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<div className="h-5 w-5 rounded-full bg-zinc-100 dark:bg-zinc-800 flex items-center justify-center">
|
||||
|
@ -378,6 +470,37 @@ export function ToolCallSidePanel({
|
|||
Step {currentIndex + 1} of {totalCalls}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isMobile ? (
|
||||
<div className="flex items-center justify-between">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={navigateToPrevious}
|
||||
disabled={currentIndex <= 0}
|
||||
className="h-9 px-3"
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4 mr-1" />
|
||||
<span>Previous</span>
|
||||
</Button>
|
||||
|
||||
<span className="text-xs text-zinc-500 dark:text-zinc-400">
|
||||
{currentIndex + 1} / {totalCalls}
|
||||
</span>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={navigateToNext}
|
||||
disabled={currentIndex >= totalCalls - 1}
|
||||
className="h-9 px-3"
|
||||
>
|
||||
<span>Next</span>
|
||||
<ChevronRight className="h-4 w-4 ml-1" />
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<Slider
|
||||
min={0}
|
||||
max={totalCalls - 1}
|
||||
|
@ -386,9 +509,9 @@ export function ToolCallSidePanel({
|
|||
onValueChange={([newValue]) => onNavigate(newValue)}
|
||||
className="w-full [&>span:first-child]:h-1 [&>span:first-child]:bg-zinc-200 dark:[&>span:first-child]:bg-zinc-800 [&>span:first-child>span]:bg-zinc-500 dark:[&>span:first-child>span]:bg-zinc-400 [&>span:first-child>span]:h-1"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue