feat: add project connection to chat
This commit is contained in:
@@ -58,6 +58,8 @@ export const MobileFlowPage = ({ screen }: MobileFlowPageProps) => {
|
|||||||
assistantError: projectWorkspace.error?.message,
|
assistantError: projectWorkspace.error?.message,
|
||||||
localStatus: workspace.statusMessage,
|
localStatus: workspace.statusMessage,
|
||||||
});
|
});
|
||||||
|
const handleRepositoryChange =
|
||||||
|
projectWorkspace.projectWorkspace.setRepository;
|
||||||
|
|
||||||
const handleOpenUnit = (selectedWorkUnitId?: string) => {
|
const handleOpenUnit = (selectedWorkUnitId?: string) => {
|
||||||
const targetId =
|
const targetId =
|
||||||
@@ -135,6 +137,7 @@ export const MobileFlowPage = ({ screen }: MobileFlowPageProps) => {
|
|||||||
data={projectWorkspace.data}
|
data={projectWorkspace.data}
|
||||||
modelId={MODEL_ID}
|
modelId={MODEL_ID}
|
||||||
modelLabel={MODEL_LABEL}
|
modelLabel={MODEL_LABEL}
|
||||||
|
onAddProject={() => workspace.setProjectManagerOpen(true)}
|
||||||
onComposerChange={workspace.handleComposerChange}
|
onComposerChange={workspace.handleComposerChange}
|
||||||
onComposerSubmit={workspace.handleComposerSubmit}
|
onComposerSubmit={workspace.handleComposerSubmit}
|
||||||
onOpenUnit={(selectedWorkUnitId) =>
|
onOpenUnit={(selectedWorkUnitId) =>
|
||||||
@@ -145,6 +148,12 @@ export const MobileFlowPage = ({ screen }: MobileFlowPageProps) => {
|
|||||||
projectId as Id<"projects">
|
projectId as Id<"projects">
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
onProjectManagerClose={() => workspace.setProjectManagerOpen(false)}
|
||||||
|
onRepositoryChange={handleRepositoryChange}
|
||||||
|
onRepositoryConnect={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
void projectWorkspace.projectWorkspace.connectRepository();
|
||||||
|
}}
|
||||||
onRestoreChecked={handleRestoreChecked}
|
onRestoreChecked={handleRestoreChecked}
|
||||||
onSettingsClose={() => workspace.setSettingsOpen(false)}
|
onSettingsClose={() => workspace.setSettingsOpen(false)}
|
||||||
onSettingsOpen={() => workspace.setSettingsOpen(true)}
|
onSettingsOpen={() => workspace.setSettingsOpen(true)}
|
||||||
@@ -153,6 +162,9 @@ export const MobileFlowPage = ({ screen }: MobileFlowPageProps) => {
|
|||||||
}}
|
}}
|
||||||
onWorkUnitChecked={handleWorkUnitChecked}
|
onWorkUnitChecked={handleWorkUnitChecked}
|
||||||
onWorkUnitSentBack={handleWorkUnitSentBack}
|
onWorkUnitSentBack={handleWorkUnitSentBack}
|
||||||
|
pendingAction={projectWorkspace.projectWorkspace.pendingAction}
|
||||||
|
projectManagerOpen={workspace.projectManagerOpen}
|
||||||
|
repositoryValue={projectWorkspace.projectWorkspace.repository}
|
||||||
settingsOpen={workspace.settingsOpen}
|
settingsOpen={workspace.settingsOpen}
|
||||||
statusMessage={statusMessage}
|
statusMessage={statusMessage}
|
||||||
userEmail={user?.email}
|
userEmail={user?.email}
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
import { LoaderCircle, X } from "lucide-react";
|
||||||
|
import type { FormEvent } from "react";
|
||||||
|
|
||||||
|
import type { MobileWorkspaceView } from "./models";
|
||||||
|
|
||||||
|
export interface MobileProjectManagerPanelProps {
|
||||||
|
readonly data: MobileWorkspaceView;
|
||||||
|
readonly onClose: () => void;
|
||||||
|
readonly onProjectSelect: (projectId: string) => void;
|
||||||
|
readonly onRepositoryChange: (value: string) => void;
|
||||||
|
readonly onRepositoryConnect: (event: FormEvent<HTMLFormElement>) => void;
|
||||||
|
readonly pendingAction?: string | null;
|
||||||
|
readonly repositoryValue: string;
|
||||||
|
readonly statusMessage?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MobileProjectManagerPanel = ({
|
||||||
|
data,
|
||||||
|
onClose,
|
||||||
|
onProjectSelect,
|
||||||
|
onRepositoryChange,
|
||||||
|
onRepositoryConnect,
|
||||||
|
pendingAction,
|
||||||
|
repositoryValue,
|
||||||
|
statusMessage,
|
||||||
|
}: MobileProjectManagerPanelProps) => {
|
||||||
|
const connecting = pendingAction === "connect";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
aria-label="Add project"
|
||||||
|
className="absolute inset-0 z-40 flex flex-col bg-[#f7f8f5] text-[#11110f]"
|
||||||
|
>
|
||||||
|
<header className="flex h-20 shrink-0 items-center border-b border-[#e9eae6] bg-white px-4">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-[18px] font-semibold">Projects</h2>
|
||||||
|
<p className="text-[11px] text-[#807b76]">
|
||||||
|
Connect a repository to Zopu.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
aria-label="Close projects"
|
||||||
|
className="ml-auto flex size-10 items-center justify-center rounded-full bg-[#f2f3ef]"
|
||||||
|
onClick={onClose}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<X className="size-5" />
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="min-h-0 flex-1 overflow-y-auto p-4">
|
||||||
|
<div className="rounded-[24px] bg-white p-4">
|
||||||
|
<label
|
||||||
|
className="text-[10px] font-semibold tracking-[0.08em] text-[#807b76] uppercase"
|
||||||
|
htmlFor="stack-project-selector"
|
||||||
|
>
|
||||||
|
Connected project
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
className="mt-2 h-11 w-full rounded-[14px] bg-[#f2f3ef] px-3 text-[13px] font-semibold outline-none focus-visible:ring-2 focus-visible:ring-[#315dc0]"
|
||||||
|
disabled={data.projects.length === 0}
|
||||||
|
id="stack-project-selector"
|
||||||
|
onChange={(event) => onProjectSelect(event.currentTarget.value)}
|
||||||
|
value={data.selectedProjectId ?? ""}
|
||||||
|
>
|
||||||
|
{data.projects.length === 0 ? (
|
||||||
|
<option value="">No connected projects</option>
|
||||||
|
) : (
|
||||||
|
data.projects.map((project) => (
|
||||||
|
<option key={project.id} value={project.id}>
|
||||||
|
{project.repositoryPath ?? project.name}
|
||||||
|
</option>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<form className="mt-5" onSubmit={onRepositoryConnect}>
|
||||||
|
<label
|
||||||
|
className="text-[10px] font-semibold tracking-[0.08em] text-[#807b76] uppercase"
|
||||||
|
htmlFor="stack-repository-url"
|
||||||
|
>
|
||||||
|
Add project
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
aria-label="Git repository URL"
|
||||||
|
className="mt-2 h-12 w-full rounded-[14px] bg-[#f2f3ef] px-3 text-[12px] outline-none placeholder:text-[#aaa7a3] focus-visible:ring-2 focus-visible:ring-[#315dc0]"
|
||||||
|
disabled={connecting}
|
||||||
|
id="stack-repository-url"
|
||||||
|
onChange={(event) =>
|
||||||
|
onRepositoryChange(event.currentTarget.value)
|
||||||
|
}
|
||||||
|
placeholder="https://git.openputer.com/owner/repo.git"
|
||||||
|
required
|
||||||
|
type="url"
|
||||||
|
value={repositoryValue}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className="mt-3 flex h-11 w-full items-center justify-center gap-2 rounded-[14px] bg-[#c8ff00] text-[13px] font-semibold text-black disabled:opacity-50"
|
||||||
|
disabled={connecting || !repositoryValue.trim()}
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
{connecting ? (
|
||||||
|
<LoaderCircle className="size-4 animate-spin" />
|
||||||
|
) : null}
|
||||||
|
{connecting ? "Connecting…" : "Connect project"}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{statusMessage ? (
|
||||||
|
<p className="mt-3 text-center text-[11px] text-[#807b76]">
|
||||||
|
{statusMessage}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
import { cn } from "@code/ui/lib/utils";
|
import { cn } from "@code/ui/lib/utils";
|
||||||
import { ChevronDown } from "lucide-react";
|
import { ChevronDown, Plus } from "lucide-react";
|
||||||
|
|
||||||
import type { MobileProjectView } from "./models";
|
import type { MobileProjectView } from "./models";
|
||||||
|
|
||||||
export interface MobileProjectSelectProps {
|
export interface MobileProjectSelectProps {
|
||||||
readonly activeCount: number;
|
readonly activeCount: number;
|
||||||
readonly className?: string;
|
readonly className?: string;
|
||||||
|
readonly onAddProject: () => void;
|
||||||
readonly onProjectSelect: (projectId: string) => void;
|
readonly onProjectSelect: (projectId: string) => void;
|
||||||
readonly projects: readonly MobileProjectView[];
|
readonly projects: readonly MobileProjectView[];
|
||||||
readonly selectedProjectId?: string;
|
readonly selectedProjectId?: string;
|
||||||
@@ -14,6 +15,7 @@ export interface MobileProjectSelectProps {
|
|||||||
export const MobileProjectSelect = ({
|
export const MobileProjectSelect = ({
|
||||||
activeCount,
|
activeCount,
|
||||||
className,
|
className,
|
||||||
|
onAddProject,
|
||||||
onProjectSelect,
|
onProjectSelect,
|
||||||
projects,
|
projects,
|
||||||
selectedProjectId,
|
selectedProjectId,
|
||||||
@@ -49,5 +51,14 @@ export const MobileProjectSelect = ({
|
|||||||
</label>
|
</label>
|
||||||
<span aria-hidden="true">·</span>
|
<span aria-hidden="true">·</span>
|
||||||
<span className="shrink-0 tabular-nums">{activeCount} active</span>
|
<span className="shrink-0 tabular-nums">{activeCount} active</span>
|
||||||
|
<button
|
||||||
|
aria-label="Add project"
|
||||||
|
className="ml-0.5 flex size-5 shrink-0 items-center justify-center rounded-full bg-[#c8ff00] text-black"
|
||||||
|
onClick={onAddProject}
|
||||||
|
title="Add project"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<Plus className="size-3.5" strokeWidth={2.5} />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import type { FormEvent, MouseEvent } from "react";
|
import type { FormEvent, MouseEvent } from "react";
|
||||||
|
|
||||||
|
import { MobileProjectManagerPanel } from "./mobile-project-manager-panel";
|
||||||
import { MobileProjectSelect } from "./mobile-project-select";
|
import { MobileProjectSelect } from "./mobile-project-select";
|
||||||
import { MobileSettingsPanel } from "./mobile-settings-panel";
|
import { MobileSettingsPanel } from "./mobile-settings-panel";
|
||||||
import type {
|
import type {
|
||||||
@@ -42,6 +43,7 @@ const dateLabel = new Intl.DateTimeFormat("en", {
|
|||||||
|
|
||||||
interface WorkStackHeaderProps {
|
interface WorkStackHeaderProps {
|
||||||
readonly activeCount: number;
|
readonly activeCount: number;
|
||||||
|
readonly onAddProject: () => void;
|
||||||
readonly onOpenSettings?: () => void;
|
readonly onOpenSettings?: () => void;
|
||||||
readonly onProjectSelect: (projectId: string) => void;
|
readonly onProjectSelect: (projectId: string) => void;
|
||||||
readonly projects: readonly MobileProjectView[];
|
readonly projects: readonly MobileProjectView[];
|
||||||
@@ -51,6 +53,7 @@ interface WorkStackHeaderProps {
|
|||||||
|
|
||||||
const WorkStackHeader = ({
|
const WorkStackHeader = ({
|
||||||
activeCount,
|
activeCount,
|
||||||
|
onAddProject,
|
||||||
onOpenSettings,
|
onOpenSettings,
|
||||||
onProjectSelect,
|
onProjectSelect,
|
||||||
projects,
|
projects,
|
||||||
@@ -67,6 +70,7 @@ const WorkStackHeader = ({
|
|||||||
</h1>
|
</h1>
|
||||||
<MobileProjectSelect
|
<MobileProjectSelect
|
||||||
activeCount={activeCount}
|
activeCount={activeCount}
|
||||||
|
onAddProject={onAddProject}
|
||||||
onProjectSelect={onProjectSelect}
|
onProjectSelect={onProjectSelect}
|
||||||
projects={projects}
|
projects={projects}
|
||||||
selectedProjectId={selectedProjectId}
|
selectedProjectId={selectedProjectId}
|
||||||
@@ -298,16 +302,23 @@ export interface MobileWorkStackScreenProps {
|
|||||||
readonly data: MobileWorkspaceView;
|
readonly data: MobileWorkspaceView;
|
||||||
readonly modelId: string;
|
readonly modelId: string;
|
||||||
readonly modelLabel: string;
|
readonly modelLabel: string;
|
||||||
|
readonly onAddProject: () => void;
|
||||||
readonly onComposerChange: (value: string) => void;
|
readonly onComposerChange: (value: string) => void;
|
||||||
readonly onComposerSubmit: (event: FormEvent<HTMLFormElement>) => void;
|
readonly onComposerSubmit: (event: FormEvent<HTMLFormElement>) => void;
|
||||||
readonly onOpenUnit: (workUnitId: string) => void;
|
readonly onOpenUnit: (workUnitId: string) => void;
|
||||||
readonly onProjectSelect: (projectId: string) => void;
|
readonly onProjectSelect: (projectId: string) => void;
|
||||||
|
readonly onProjectManagerClose: () => void;
|
||||||
|
readonly onRepositoryChange: (value: string) => void;
|
||||||
|
readonly onRepositoryConnect: (event: FormEvent<HTMLFormElement>) => void;
|
||||||
readonly onRestoreChecked: () => void;
|
readonly onRestoreChecked: () => void;
|
||||||
readonly onSettingsClose: () => void;
|
readonly onSettingsClose: () => void;
|
||||||
readonly onSettingsOpen: () => void;
|
readonly onSettingsOpen: () => void;
|
||||||
readonly onSignOut?: () => void;
|
readonly onSignOut?: () => void;
|
||||||
readonly onWorkUnitChecked: (workUnitId: string) => void;
|
readonly onWorkUnitChecked: (workUnitId: string) => void;
|
||||||
readonly onWorkUnitSentBack: (workUnitId: string) => void;
|
readonly onWorkUnitSentBack: (workUnitId: string) => void;
|
||||||
|
readonly pendingAction?: string | null;
|
||||||
|
readonly projectManagerOpen: boolean;
|
||||||
|
readonly repositoryValue: string;
|
||||||
readonly settingsOpen: boolean;
|
readonly settingsOpen: boolean;
|
||||||
readonly statusMessage?: string;
|
readonly statusMessage?: string;
|
||||||
readonly userEmail?: string;
|
readonly userEmail?: string;
|
||||||
@@ -322,16 +333,23 @@ export const MobileWorkStackScreen = ({
|
|||||||
data,
|
data,
|
||||||
modelId,
|
modelId,
|
||||||
modelLabel,
|
modelLabel,
|
||||||
|
onAddProject,
|
||||||
onComposerChange,
|
onComposerChange,
|
||||||
onComposerSubmit,
|
onComposerSubmit,
|
||||||
onOpenUnit,
|
onOpenUnit,
|
||||||
|
onProjectManagerClose,
|
||||||
onProjectSelect,
|
onProjectSelect,
|
||||||
|
onRepositoryChange,
|
||||||
|
onRepositoryConnect,
|
||||||
onRestoreChecked,
|
onRestoreChecked,
|
||||||
onSettingsClose,
|
onSettingsClose,
|
||||||
onSettingsOpen,
|
onSettingsOpen,
|
||||||
onSignOut,
|
onSignOut,
|
||||||
onWorkUnitChecked,
|
onWorkUnitChecked,
|
||||||
onWorkUnitSentBack,
|
onWorkUnitSentBack,
|
||||||
|
pendingAction,
|
||||||
|
projectManagerOpen,
|
||||||
|
repositoryValue,
|
||||||
settingsOpen,
|
settingsOpen,
|
||||||
statusMessage,
|
statusMessage,
|
||||||
userEmail,
|
userEmail,
|
||||||
@@ -350,6 +368,7 @@ export const MobileWorkStackScreen = ({
|
|||||||
<div className="relative mx-auto flex h-[100dvh] max-h-[858px] min-h-[700px] w-full max-w-[390px] flex-col overflow-hidden bg-white text-[#0b0b0a]">
|
<div className="relative mx-auto flex h-[100dvh] max-h-[858px] min-h-[700px] w-full max-w-[390px] flex-col overflow-hidden bg-white text-[#0b0b0a]">
|
||||||
<WorkStackHeader
|
<WorkStackHeader
|
||||||
activeCount={data.activeCount}
|
activeCount={data.activeCount}
|
||||||
|
onAddProject={onAddProject}
|
||||||
onOpenSettings={onSettingsOpen}
|
onOpenSettings={onSettingsOpen}
|
||||||
onProjectSelect={onProjectSelect}
|
onProjectSelect={onProjectSelect}
|
||||||
projects={data.projects}
|
projects={data.projects}
|
||||||
@@ -458,6 +477,18 @@ export const MobileWorkStackScreen = ({
|
|||||||
statusMessage={statusMessage}
|
statusMessage={statusMessage}
|
||||||
value={composerValue}
|
value={composerValue}
|
||||||
/>
|
/>
|
||||||
|
{projectManagerOpen ? (
|
||||||
|
<MobileProjectManagerPanel
|
||||||
|
data={data}
|
||||||
|
onClose={onProjectManagerClose}
|
||||||
|
onProjectSelect={onProjectSelect}
|
||||||
|
onRepositoryChange={onRepositoryChange}
|
||||||
|
onRepositoryConnect={onRepositoryConnect}
|
||||||
|
pendingAction={pendingAction}
|
||||||
|
repositoryValue={repositoryValue}
|
||||||
|
statusMessage={statusMessage}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
{settingsOpen ? (
|
{settingsOpen ? (
|
||||||
<MobileSettingsPanel
|
<MobileSettingsPanel
|
||||||
modelId={modelId}
|
modelId={modelId}
|
||||||
|
|||||||
Reference in New Issue
Block a user