mirror of
https://github.com/run-llama/auto_rfp.git
synced 2026-07-01 21:54:05 -04:00
Merge pull request #20 from run-llama/frontend_improvement_continued
Frontend improvement continued
This commit is contained in:
@@ -1,29 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
|
||||
export default function LogoutButton() {
|
||||
return (
|
||||
<Link
|
||||
href="/logout"
|
||||
className="flex items-center gap-2 rounded-md px-3 py-2 text-sm font-medium hover:bg-gray-100"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
|
||||
<polyline points="16 17 21 12 16 7" />
|
||||
<line x1="21" y1="12" x2="9" y2="12" />
|
||||
</svg>
|
||||
Log out
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
+79
-110
@@ -3,82 +3,44 @@
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-ring: var(--ring);
|
||||
--color-input: var(--input);
|
||||
--color-border: var(--border);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-card: var(--card);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
}
|
||||
|
||||
:root {
|
||||
--background: oklch(0.9821 0 0);
|
||||
--foreground: oklch(0.2435 0 0);
|
||||
--card: oklch(0.9911 0 0);
|
||||
--card-foreground: oklch(0.2435 0 0);
|
||||
--popover: oklch(0.9911 0 0);
|
||||
--popover-foreground: oklch(0.2435 0 0);
|
||||
--primary: oklch(0.4341 0.0392 41.9938);
|
||||
--background: oklch(1.0000 0 0);
|
||||
--foreground: oklch(0.3211 0 0);
|
||||
--card: oklch(1.0000 0 0);
|
||||
--card-foreground: oklch(0.3211 0 0);
|
||||
--popover: oklch(1.0000 0 0);
|
||||
--popover-foreground: oklch(0.3211 0 0);
|
||||
--primary: oklch(0.6231 0.1880 259.8145);
|
||||
--primary-foreground: oklch(1.0000 0 0);
|
||||
--secondary: oklch(0.9200 0.0651 74.3695);
|
||||
--secondary-foreground: oklch(0.3499 0.0685 40.8288);
|
||||
--muted: oklch(0.9521 0 0);
|
||||
--muted-foreground: oklch(0.5032 0 0);
|
||||
--accent: oklch(0.9310 0 0);
|
||||
--accent-foreground: oklch(0.2435 0 0);
|
||||
--destructive: oklch(0.6271 0.1936 33.3390);
|
||||
--secondary: oklch(0.9670 0.0029 264.5419);
|
||||
--secondary-foreground: oklch(0.4461 0.0263 256.8018);
|
||||
--muted: oklch(0.9846 0.0017 247.8389);
|
||||
--muted-foreground: oklch(0.5510 0.0234 264.3637);
|
||||
--accent: oklch(0.9514 0.0250 236.8242);
|
||||
--accent-foreground: oklch(0.3791 0.1378 265.5222);
|
||||
--destructive: oklch(0.6368 0.2078 25.3313);
|
||||
--destructive-foreground: oklch(1.0000 0 0);
|
||||
--border: oklch(0.8822 0 0);
|
||||
--input: oklch(0.8822 0 0);
|
||||
--ring: oklch(0.4341 0.0392 41.9938);
|
||||
--chart-1: oklch(0.4341 0.0392 41.9938);
|
||||
--chart-2: oklch(0.9200 0.0651 74.3695);
|
||||
--chart-3: oklch(0.9310 0 0);
|
||||
--chart-4: oklch(0.9367 0.0523 75.5009);
|
||||
--chart-5: oklch(0.4338 0.0437 41.6746);
|
||||
--sidebar: oklch(0.9881 0 0);
|
||||
--sidebar-foreground: oklch(0.2645 0 0);
|
||||
--sidebar-primary: oklch(0.3250 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.9881 0 0);
|
||||
--sidebar-accent: oklch(0.9761 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.3250 0 0);
|
||||
--sidebar-border: oklch(0.9401 0 0);
|
||||
--sidebar-ring: oklch(0.7731 0 0);
|
||||
--font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
|
||||
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
--radius: 0.5rem;
|
||||
--border: oklch(0.9276 0.0058 264.5313);
|
||||
--input: oklch(0.9276 0.0058 264.5313);
|
||||
--ring: oklch(0.6231 0.1880 259.8145);
|
||||
--chart-1: oklch(0.6231 0.1880 259.8145);
|
||||
--chart-2: oklch(0.5461 0.2152 262.8809);
|
||||
--chart-3: oklch(0.4882 0.2172 264.3763);
|
||||
--chart-4: oklch(0.4244 0.1809 265.6377);
|
||||
--chart-5: oklch(0.3791 0.1378 265.5222);
|
||||
--sidebar: oklch(0.9846 0.0017 247.8389);
|
||||
--sidebar-foreground: oklch(0.3211 0 0);
|
||||
--sidebar-primary: oklch(0.6231 0.1880 259.8145);
|
||||
--sidebar-primary-foreground: oklch(1.0000 0 0);
|
||||
--sidebar-accent: oklch(0.9514 0.0250 236.8242);
|
||||
--sidebar-accent-foreground: oklch(0.3791 0.1378 265.5222);
|
||||
--sidebar-border: oklch(0.9276 0.0058 264.5313);
|
||||
--sidebar-ring: oklch(0.6231 0.1880 259.8145);
|
||||
--font-sans: Inter, sans-serif;
|
||||
--font-serif: Source Serif 4, serif;
|
||||
--font-mono: JetBrains Mono, monospace;
|
||||
--radius: 0.375rem;
|
||||
--shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
|
||||
--shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
|
||||
--shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
|
||||
@@ -87,47 +49,45 @@
|
||||
--shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 4px 6px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 8px 10px -1px hsl(0 0% 0% / 0.10);
|
||||
--shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25);
|
||||
--tracking-normal: 0em;
|
||||
--spacing: 0.25rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.1776 0 0);
|
||||
--foreground: oklch(0.9491 0 0);
|
||||
--card: oklch(0.2134 0 0);
|
||||
--card-foreground: oklch(0.9491 0 0);
|
||||
--popover: oklch(0.2134 0 0);
|
||||
--popover-foreground: oklch(0.9491 0 0);
|
||||
--primary: oklch(0.9247 0.0524 66.1732);
|
||||
--primary-foreground: oklch(0.2029 0.0240 200.1962);
|
||||
--secondary: oklch(0.3163 0.0190 63.6992);
|
||||
--secondary-foreground: oklch(0.9247 0.0524 66.1732);
|
||||
--muted: oklch(0.2520 0 0);
|
||||
--muted-foreground: oklch(0.7699 0 0);
|
||||
--accent: oklch(0.2850 0 0);
|
||||
--accent-foreground: oklch(0.9491 0 0);
|
||||
--destructive: oklch(0.6271 0.1936 33.3390);
|
||||
--background: oklch(0.2046 0 0);
|
||||
--foreground: oklch(0.9219 0 0);
|
||||
--card: oklch(0.2686 0 0);
|
||||
--card-foreground: oklch(0.9219 0 0);
|
||||
--popover: oklch(0.2686 0 0);
|
||||
--popover-foreground: oklch(0.9219 0 0);
|
||||
--primary: oklch(0.6231 0.1880 259.8145);
|
||||
--primary-foreground: oklch(1.0000 0 0);
|
||||
--secondary: oklch(0.2686 0 0);
|
||||
--secondary-foreground: oklch(0.9219 0 0);
|
||||
--muted: oklch(0.2686 0 0);
|
||||
--muted-foreground: oklch(0.7155 0 0);
|
||||
--accent: oklch(0.3791 0.1378 265.5222);
|
||||
--accent-foreground: oklch(0.8823 0.0571 254.1284);
|
||||
--destructive: oklch(0.6368 0.2078 25.3313);
|
||||
--destructive-foreground: oklch(1.0000 0 0);
|
||||
--border: oklch(0.2351 0.0115 91.7467);
|
||||
--input: oklch(0.4017 0 0);
|
||||
--ring: oklch(0.9247 0.0524 66.1732);
|
||||
--chart-1: oklch(0.9247 0.0524 66.1732);
|
||||
--chart-2: oklch(0.3163 0.0190 63.6992);
|
||||
--chart-3: oklch(0.2850 0 0);
|
||||
--chart-4: oklch(0.3481 0.0219 67.0001);
|
||||
--chart-5: oklch(0.9245 0.0533 67.0855);
|
||||
--sidebar: oklch(0.2103 0.0059 285.8852);
|
||||
--sidebar-foreground: oklch(0.9674 0.0013 286.3752);
|
||||
--sidebar-primary: oklch(0.4882 0.2172 264.3763);
|
||||
--border: oklch(0.3715 0 0);
|
||||
--input: oklch(0.3715 0 0);
|
||||
--ring: oklch(0.6231 0.1880 259.8145);
|
||||
--chart-1: oklch(0.7137 0.1434 254.6240);
|
||||
--chart-2: oklch(0.6231 0.1880 259.8145);
|
||||
--chart-3: oklch(0.5461 0.2152 262.8809);
|
||||
--chart-4: oklch(0.4882 0.2172 264.3763);
|
||||
--chart-5: oklch(0.4244 0.1809 265.6377);
|
||||
--sidebar: oklch(0.2046 0 0);
|
||||
--sidebar-foreground: oklch(0.9219 0 0);
|
||||
--sidebar-primary: oklch(0.6231 0.1880 259.8145);
|
||||
--sidebar-primary-foreground: oklch(1.0000 0 0);
|
||||
--sidebar-accent: oklch(0.2739 0.0055 286.0326);
|
||||
--sidebar-accent-foreground: oklch(0.9674 0.0013 286.3752);
|
||||
--sidebar-border: oklch(0.2739 0.0055 286.0326);
|
||||
--sidebar-ring: oklch(0.8711 0.0055 286.2860);
|
||||
--font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
|
||||
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
--radius: 0.5rem;
|
||||
--sidebar-accent: oklch(0.3791 0.1378 265.5222);
|
||||
--sidebar-accent-foreground: oklch(0.8823 0.0571 254.1284);
|
||||
--sidebar-border: oklch(0.3715 0 0);
|
||||
--sidebar-ring: oklch(0.6231 0.1880 259.8145);
|
||||
--font-sans: Inter, sans-serif;
|
||||
--font-serif: Source Serif 4, serif;
|
||||
--font-mono: JetBrains Mono, monospace;
|
||||
--radius: 0.375rem;
|
||||
--shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
|
||||
--shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
|
||||
--shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
|
||||
@@ -189,4 +149,13 @@
|
||||
--shadow-lg: var(--shadow-lg);
|
||||
--shadow-xl: var(--shadow-xl);
|
||||
--shadow-2xl: var(--shadow-2xl);
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
+9
-4
@@ -2,7 +2,8 @@ import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import { Toaster } from "@/components/ui/toaster";
|
||||
import { Header } from "@/components/global/header";
|
||||
import { GlobalHeader } from "@/components/global/global-header";
|
||||
import { Providers } from "@/providers/providers";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
@@ -29,9 +30,13 @@ export default function RootLayout({
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
<Header />
|
||||
{children}
|
||||
<Toaster />
|
||||
<Providers>
|
||||
<div className="flex flex-col">
|
||||
<GlobalHeader />
|
||||
{children}
|
||||
</div>
|
||||
<Toaster />
|
||||
</Providers>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@@ -5,5 +5,9 @@ export default function OrganizationsLayout({
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return <SidebarLayout>{children}</SidebarLayout>;
|
||||
return (
|
||||
|
||||
<SidebarLayout>{children}</SidebarLayout>
|
||||
|
||||
);
|
||||
}
|
||||
@@ -202,7 +202,7 @@ export default function OrganizationsPage() {
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-7xl mx-auto">
|
||||
<div className="py-6 px-4 sm:px-6">
|
||||
<div className="py-6 px-4 sm:px-6 pt-20">
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
|
||||
@@ -1,8 +1,19 @@
|
||||
export { IndexSelector } from './index-selector';
|
||||
export { QuestionsHeader } from './questions-header';
|
||||
export { NoQuestionsAvailable } from './no-questions-available';
|
||||
export { SourceDetailsDialog } from './source-details-dialog';
|
||||
export { QuestionEditor } from './question-editor';
|
||||
export { QuestionsFilter } from './questions-filter';
|
||||
export { QuestionsTabsContent } from './questions-tabs-content';
|
||||
export { QuestionsSection } from './questions-section';
|
||||
// Main components
|
||||
export { QuestionsSection } from "./questions-section"
|
||||
export { QuestionsProvider, useQuestions } from "./questions-provider"
|
||||
|
||||
// Sub-components
|
||||
export { QuestionsHeader } from "./questions-header"
|
||||
export { QuestionsFilterTabs } from "./questions-filter-tabs"
|
||||
export { QuestionsTabsContent } from "./questions-tabs-content"
|
||||
export { NoQuestionsAvailable } from "./no-questions-available"
|
||||
export { SourceDetailsDialog } from "./source-details-dialog"
|
||||
|
||||
// State components
|
||||
export { QuestionsLoadingState, QuestionsErrorState, QuestionsSkeletonLoader } from "./questions-states"
|
||||
|
||||
// Dialog handlers
|
||||
export { MultiStepResponseHandler } from "./multi-step-response-handler"
|
||||
|
||||
// Commented out components (available if needed)
|
||||
// export { IndexSelector } from "./index-selector"
|
||||
@@ -0,0 +1,31 @@
|
||||
"use client"
|
||||
|
||||
import React from "react"
|
||||
import { MultiStepResponseDialog } from "@/components/ui/multi-step-response-dialog"
|
||||
import { useQuestions } from "./questions-provider"
|
||||
|
||||
export function MultiStepResponseHandler() {
|
||||
const {
|
||||
multiStepDialogOpen,
|
||||
currentQuestionText,
|
||||
isMultiStepGenerating,
|
||||
multiStepSteps,
|
||||
multiStepFinalResponse,
|
||||
multiStepSources,
|
||||
handleAcceptMultiStepResponse,
|
||||
handleCloseMultiStepDialog,
|
||||
} = useQuestions();
|
||||
|
||||
return (
|
||||
<MultiStepResponseDialog
|
||||
isOpen={multiStepDialogOpen}
|
||||
onClose={handleCloseMultiStepDialog}
|
||||
questionText={currentQuestionText || ""}
|
||||
isGenerating={isMultiStepGenerating}
|
||||
currentSteps={multiStepSteps}
|
||||
finalResponse={multiStepFinalResponse || ""}
|
||||
sources={multiStepSources}
|
||||
onAcceptResponse={handleAcceptMultiStepResponse}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -9,6 +9,10 @@ interface NoQuestionsAvailableProps {
|
||||
}
|
||||
|
||||
export function NoQuestionsAvailable({ projectId }: NoQuestionsAvailableProps) {
|
||||
|
||||
console.log("In NoQuestionsAvailable, projectId", projectId);
|
||||
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const handleUploadClick = () => {
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
"use client"
|
||||
|
||||
import React from "react"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { useQuestions } from "./questions-provider"
|
||||
import { QuestionsTabsContent } from "./questions-tabs-content"
|
||||
|
||||
interface QuestionsFilterTabsProps {
|
||||
rfpDocument: any;
|
||||
}
|
||||
|
||||
export function QuestionsFilterTabs({ rfpDocument }: QuestionsFilterTabsProps) {
|
||||
const {
|
||||
activeTab,
|
||||
setActiveTab,
|
||||
selectedQuestion,
|
||||
getSelectedQuestionData,
|
||||
answers,
|
||||
unsavedQuestions,
|
||||
selectedIndexes,
|
||||
isGenerating,
|
||||
isMultiStepGenerating,
|
||||
savingQuestions,
|
||||
useMultiStep,
|
||||
showAIPanel,
|
||||
setSelectedQuestion,
|
||||
setShowAIPanel,
|
||||
handleAnswerChange,
|
||||
saveAnswer,
|
||||
handleMarkComplete,
|
||||
handleGenerateAnswer,
|
||||
handleSourceClick,
|
||||
setUseMultiStep,
|
||||
getFilteredQuestions,
|
||||
getCounts,
|
||||
searchQuery,
|
||||
} = useQuestions();
|
||||
|
||||
const questionData = getSelectedQuestionData();
|
||||
const counts = getCounts();
|
||||
|
||||
return (
|
||||
<Tabs defaultValue="all" value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList className="grid w-full grid-cols-4 mb-4">
|
||||
<TabsTrigger value="all" className="gap-1">
|
||||
All Questions
|
||||
<Badge variant="secondary" className="ml-1">{counts.all}</Badge>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="answered" className="gap-1">
|
||||
Answered
|
||||
<Badge variant="secondary" className="ml-1">{counts.answered}</Badge>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="unanswered" className="gap-1">
|
||||
Unanswered
|
||||
<Badge variant="secondary" className="ml-1">{counts.unanswered}</Badge>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="flagged" className="gap-1">
|
||||
Needs Review
|
||||
<Badge variant="secondary" className="ml-1">{counts.flagged}</Badge>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{["all", "answered", "unanswered", "flagged"].map(filterType => (
|
||||
<TabsContent key={filterType} value={filterType} className="space-y-4">
|
||||
<QuestionsTabsContent
|
||||
questions={getFilteredQuestions(filterType)}
|
||||
selectedQuestion={selectedQuestion}
|
||||
questionData={questionData}
|
||||
answers={answers}
|
||||
unsavedQuestions={unsavedQuestions}
|
||||
selectedIndexes={selectedIndexes}
|
||||
isGenerating={isGenerating}
|
||||
isMultiStepGenerating={isMultiStepGenerating}
|
||||
savingQuestions={savingQuestions}
|
||||
useMultiStep={useMultiStep}
|
||||
showAIPanel={showAIPanel}
|
||||
filterType={filterType}
|
||||
onSelectQuestion={(id) => {
|
||||
setSelectedQuestion(id);
|
||||
setShowAIPanel(false);
|
||||
}}
|
||||
onAnswerChange={handleAnswerChange}
|
||||
onSave={saveAnswer}
|
||||
onMarkComplete={handleMarkComplete}
|
||||
onGenerateAnswer={handleGenerateAnswer}
|
||||
onSourceClick={handleSourceClick}
|
||||
onMultiStepToggle={setUseMultiStep}
|
||||
rfpDocument={rfpDocument}
|
||||
searchQuery={searchQuery}
|
||||
/>
|
||||
</TabsContent>
|
||||
))}
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,713 @@
|
||||
"use client"
|
||||
|
||||
import React, { useState, useEffect, createContext, useContext, ReactNode } from "react"
|
||||
import { toast } from "@/components/ui/use-toast"
|
||||
import { RfpDocument, AnswerSource } from "@/types/api"
|
||||
import { useMultiStepResponse } from "@/hooks/use-multi-step-response"
|
||||
|
||||
// Interfaces
|
||||
interface AnswerData {
|
||||
text: string;
|
||||
sources?: AnswerSource[];
|
||||
}
|
||||
|
||||
interface ProjectIndex {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface QuestionsContextType {
|
||||
// UI state
|
||||
showAIPanel: boolean;
|
||||
setShowAIPanel: (show: boolean) => void;
|
||||
selectedQuestion: string | null;
|
||||
setSelectedQuestion: (id: string | null) => void;
|
||||
activeTab: string;
|
||||
setActiveTab: (tab: string) => void;
|
||||
|
||||
// Data state
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
rfpDocument: RfpDocument | null;
|
||||
project: any;
|
||||
answers: Record<string, AnswerData>;
|
||||
unsavedQuestions: Set<string>;
|
||||
|
||||
// Process state
|
||||
savingQuestions: Set<string>;
|
||||
lastSaved: string | null;
|
||||
isGenerating: Record<string, boolean>;
|
||||
searchQuery: string;
|
||||
setSearchQuery: (query: string) => void;
|
||||
selectedSource: AnswerSource | null;
|
||||
setSelectedSource: (source: AnswerSource | null) => void;
|
||||
isSourceModalOpen: boolean;
|
||||
setIsSourceModalOpen: (open: boolean) => void;
|
||||
selectedIndexes: Set<string>;
|
||||
setSelectedIndexes: (indexes: Set<string>) => void;
|
||||
availableIndexes: ProjectIndex[];
|
||||
isLoadingIndexes: boolean;
|
||||
organizationConnected: boolean;
|
||||
|
||||
// Multi-step response state
|
||||
useMultiStep: boolean;
|
||||
setUseMultiStep: (use: boolean) => void;
|
||||
multiStepDialogOpen: boolean;
|
||||
setMultiStepDialogOpen: (open: boolean) => void;
|
||||
currentQuestionForMultiStep: string | null;
|
||||
currentQuestionText: string;
|
||||
|
||||
// Multi-step response hook
|
||||
generateMultiStepResponse: (question: string) => Promise<void>;
|
||||
isMultiStepGenerating: boolean;
|
||||
multiStepSteps: any[];
|
||||
multiStepFinalResponse: string | null;
|
||||
multiStepSources: any[];
|
||||
resetMultiStepResponse: () => void;
|
||||
|
||||
// Action handlers
|
||||
handleAnswerChange: (questionId: string, value: string) => void;
|
||||
handleGenerateAnswer: (questionId: string) => Promise<void>;
|
||||
saveAnswer: (questionId: string) => Promise<void>;
|
||||
saveAllAnswers: () => Promise<void>;
|
||||
handleExportAnswers: () => void;
|
||||
handleMarkComplete: (questionId: string) => void;
|
||||
handleSourceClick: (source: AnswerSource) => void;
|
||||
handleIndexToggle: (indexId: string) => void;
|
||||
handleSelectAllIndexes: () => void;
|
||||
handleAcceptMultiStepResponse: (response: string, sources: any[]) => void;
|
||||
handleCloseMultiStepDialog: () => void;
|
||||
|
||||
// Utility functions
|
||||
getFilteredQuestions: (filterType?: string) => any[];
|
||||
getCounts: () => { all: number; answered: number; unanswered: number; flagged: number };
|
||||
getSelectedQuestionData: () => any;
|
||||
}
|
||||
|
||||
const QuestionsContext = createContext<QuestionsContextType | undefined>(undefined);
|
||||
|
||||
export function useQuestions() {
|
||||
const context = useContext(QuestionsContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useQuestions must be used within a QuestionsProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
interface QuestionsProviderProps {
|
||||
children: ReactNode;
|
||||
projectId: string;
|
||||
}
|
||||
|
||||
export function QuestionsProvider({ children, projectId }: QuestionsProviderProps) {
|
||||
// UI state
|
||||
const [showAIPanel, setShowAIPanel] = useState(false);
|
||||
const [selectedQuestion, setSelectedQuestion] = useState<string | null>(null);
|
||||
const [activeTab, setActiveTab] = useState("all");
|
||||
|
||||
// Data state
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [rfpDocument, setRfpDocument] = useState<RfpDocument | null>(null);
|
||||
const [project, setProject] = useState<any>(null);
|
||||
const [answers, setAnswers] = useState<Record<string, AnswerData>>({});
|
||||
const [unsavedQuestions, setUnsavedQuestions] = useState<Set<string>>(new Set());
|
||||
|
||||
// Process state
|
||||
const [savingQuestions, setSavingQuestions] = useState<Set<string>>(new Set());
|
||||
const [lastSaved, setLastSaved] = useState<string | null>(null);
|
||||
const [isGenerating, setIsGenerating] = useState<Record<string, boolean>>({});
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [selectedSource, setSelectedSource] = useState<AnswerSource | null>(null);
|
||||
const [isSourceModalOpen, setIsSourceModalOpen] = useState(false);
|
||||
const [selectedIndexes, setSelectedIndexes] = useState<Set<string>>(new Set());
|
||||
const [availableIndexes, setAvailableIndexes] = useState<ProjectIndex[]>([]);
|
||||
const [isLoadingIndexes, setIsLoadingIndexes] = useState(false);
|
||||
const [organizationConnected, setOrganizationConnected] = useState(false);
|
||||
|
||||
// Multi-step response state
|
||||
const [useMultiStep, setUseMultiStep] = useState(false);
|
||||
const [multiStepDialogOpen, setMultiStepDialogOpen] = useState(false);
|
||||
const [currentQuestionForMultiStep, setCurrentQuestionForMultiStep] = useState<string | null>(null);
|
||||
const [currentQuestionText, setCurrentQuestionText] = useState<string>("");
|
||||
|
||||
// Use the multi-step response hook
|
||||
const {
|
||||
generateResponse: generateMultiStepResponse,
|
||||
isGenerating: isMultiStepGenerating,
|
||||
currentSteps: multiStepSteps,
|
||||
finalResponse: multiStepFinalResponse,
|
||||
sources: multiStepSources,
|
||||
reset: resetMultiStepResponse
|
||||
} = useMultiStepResponse({
|
||||
projectId: projectId || "",
|
||||
indexIds: Array.from(selectedIndexes),
|
||||
onComplete: (finalResponse, steps, sources) => {
|
||||
handleAcceptMultiStepResponse(finalResponse, sources);
|
||||
}
|
||||
});
|
||||
|
||||
// Load project data and questions when component mounts
|
||||
useEffect(() => {
|
||||
if (!projectId) {
|
||||
setError("No project ID provided");
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchProject = async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/projects/${projectId}`);
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to load project");
|
||||
}
|
||||
const data = await response.json();
|
||||
setProject(data);
|
||||
} catch (error) {
|
||||
console.error("Error loading project:", error);
|
||||
setError("Failed to load project. Please try again.");
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchIndexes = async () => {
|
||||
setIsLoadingIndexes(true);
|
||||
try {
|
||||
const response = await fetch(`/api/projects/${projectId}/indexes`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setOrganizationConnected(data.organizationConnected);
|
||||
if (data.organizationConnected) {
|
||||
const indexes = data.availableIndexes || [] as ProjectIndex[];
|
||||
setAvailableIndexes(indexes);
|
||||
|
||||
const currentIndexes = data.currentIndexes || [] as ProjectIndex[];
|
||||
const currentIndexIds = new Set(currentIndexes.map((index: ProjectIndex) => index.id)) as Set<string>;
|
||||
setSelectedIndexes(currentIndexIds);
|
||||
}
|
||||
} else {
|
||||
const errorData = await response.json().catch(() => ({ error: 'Unknown error' }));
|
||||
console.error("Error response from indexes API:", errorData);
|
||||
|
||||
if (errorData.error?.includes('Invalid index IDs')) {
|
||||
setSelectedIndexes(new Set());
|
||||
toast({
|
||||
title: "Index Sync Issue",
|
||||
description: "Some project indexes are out of sync. Please reconfigure your document indexes in project settings.",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
|
||||
setOrganizationConnected(true);
|
||||
setAvailableIndexes([]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading indexes:", error);
|
||||
setOrganizationConnected(false);
|
||||
setAvailableIndexes([]);
|
||||
setSelectedIndexes(new Set());
|
||||
} finally {
|
||||
setIsLoadingIndexes(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchQuestions = async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/questions/${projectId}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to load questions");
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
setRfpDocument(data);
|
||||
|
||||
const answersResponse = await fetch(`/api/questions/${projectId}/answers`);
|
||||
if (answersResponse.ok) {
|
||||
const savedAnswers = await answersResponse.json();
|
||||
|
||||
const normalizedAnswers: Record<string, AnswerData> = {};
|
||||
for (const [questionId, answerData] of Object.entries(savedAnswers)) {
|
||||
if (typeof answerData === 'string') {
|
||||
normalizedAnswers[questionId] = { text: answerData };
|
||||
} else {
|
||||
normalizedAnswers[questionId] = answerData as AnswerData;
|
||||
}
|
||||
}
|
||||
|
||||
setAnswers(normalizedAnswers);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading questions:", error);
|
||||
setError("Failed to load questions. Please try again.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
Promise.all([fetchProject(), fetchIndexes(), fetchQuestions()]).catch(error => {
|
||||
console.error("Error in parallel loading:", error);
|
||||
});
|
||||
}, [projectId]);
|
||||
|
||||
// Handle index selection
|
||||
const handleIndexToggle = (indexId: string) => {
|
||||
setSelectedIndexes(prev => {
|
||||
const newSelected = new Set(prev);
|
||||
if (newSelected.has(indexId)) {
|
||||
newSelected.delete(indexId);
|
||||
} else {
|
||||
newSelected.add(indexId);
|
||||
}
|
||||
return newSelected;
|
||||
});
|
||||
};
|
||||
|
||||
// Toggle all indexes
|
||||
const handleSelectAllIndexes = () => {
|
||||
if (selectedIndexes.size === availableIndexes.length) {
|
||||
setSelectedIndexes(new Set());
|
||||
} else {
|
||||
setSelectedIndexes(new Set(availableIndexes.map(index => index.id)));
|
||||
}
|
||||
};
|
||||
|
||||
// Handle answer changes
|
||||
const handleAnswerChange = (questionId: string, value: string) => {
|
||||
setAnswers(prev => {
|
||||
const existing = prev[questionId] || { text: '' };
|
||||
return {
|
||||
...prev,
|
||||
[questionId]: {
|
||||
...existing,
|
||||
text: value
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
setUnsavedQuestions(prev => {
|
||||
const updated = new Set(prev);
|
||||
updated.add(questionId);
|
||||
return updated;
|
||||
});
|
||||
};
|
||||
|
||||
// Modified generate answer handler to support multi-step
|
||||
const handleGenerateAnswer = async (questionId: string) => {
|
||||
const question = rfpDocument?.sections.flatMap(s => s.questions).find(q => q.id === questionId);
|
||||
|
||||
if (!question) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Question not found",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (useMultiStep) {
|
||||
setCurrentQuestionForMultiStep(questionId);
|
||||
setCurrentQuestionText(question.question);
|
||||
setMultiStepDialogOpen(true);
|
||||
resetMultiStepResponse();
|
||||
|
||||
if (!projectId) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Project ID not available",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await generateMultiStepResponse(question.question);
|
||||
} else {
|
||||
setIsGenerating(prev => ({ ...prev, [questionId]: true }));
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/generate-response', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
question: question.question,
|
||||
documentIds: project?.documentIds || [],
|
||||
selectedIndexIds: Array.from(selectedIndexes),
|
||||
useAllIndexes: false,
|
||||
projectId: project?.id
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to generate answer");
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
setAnswers(prev => ({
|
||||
...prev,
|
||||
[questionId]: {
|
||||
text: result.response,
|
||||
sources: result.sources
|
||||
}
|
||||
}));
|
||||
|
||||
setUnsavedQuestions(prev => {
|
||||
const updated = new Set(prev);
|
||||
updated.add(questionId);
|
||||
return updated;
|
||||
});
|
||||
|
||||
toast({
|
||||
title: "Answer Generated",
|
||||
description: "AI-generated answer has been created. Please review and save it.",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error generating answer:', error);
|
||||
toast({
|
||||
title: "Generation Error",
|
||||
description: "Failed to generate answer. Please try again.",
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setIsGenerating(prev => ({ ...prev, [questionId]: false }));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Handler for accepting multi-step response
|
||||
const handleAcceptMultiStepResponse = (response: string, sources: any[]) => {
|
||||
if (currentQuestionForMultiStep) {
|
||||
setAnswers(prev => ({
|
||||
...prev,
|
||||
[currentQuestionForMultiStep]: {
|
||||
text: response,
|
||||
sources: sources
|
||||
}
|
||||
}));
|
||||
|
||||
setUnsavedQuestions(prev => {
|
||||
const updated = new Set(prev);
|
||||
updated.add(currentQuestionForMultiStep);
|
||||
return updated;
|
||||
});
|
||||
|
||||
toast({
|
||||
title: "Multi-Step Answer Generated",
|
||||
description: "AI-generated answer with step-by-step reasoning has been created. Please review and save it.",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleCloseMultiStepDialog = () => {
|
||||
setMultiStepDialogOpen(false);
|
||||
setCurrentQuestionForMultiStep(null);
|
||||
resetMultiStepResponse();
|
||||
};
|
||||
|
||||
// Save a single answer
|
||||
const saveAnswer = async (questionId: string) => {
|
||||
if (!projectId || !answers[questionId]) return;
|
||||
|
||||
setSavingQuestions(prev => {
|
||||
const updated = new Set(prev);
|
||||
updated.add(questionId);
|
||||
return updated;
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/questions/${projectId}/answers/${questionId}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(answers[questionId]),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
setUnsavedQuestions(prev => {
|
||||
const updated = new Set(prev);
|
||||
updated.delete(questionId);
|
||||
return updated;
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
setLastSaved(result.timestamp);
|
||||
|
||||
toast({
|
||||
title: "Answer Saved",
|
||||
description: "Your answer has been saved successfully.",
|
||||
});
|
||||
} else {
|
||||
throw new Error(`Failed to save answer: ${response.statusText}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error saving answer for question ${questionId}:`, error);
|
||||
toast({
|
||||
title: "Save Error",
|
||||
description: "Failed to save your answer. Please try again.",
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setSavingQuestions(prev => {
|
||||
const updated = new Set(prev);
|
||||
updated.delete(questionId);
|
||||
return updated;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Save all unsaved answers
|
||||
const saveAllAnswers = async () => {
|
||||
if (!projectId || unsavedQuestions.size === 0) return;
|
||||
|
||||
const answersToSave: Record<string, AnswerData> = {};
|
||||
unsavedQuestions.forEach(questionId => {
|
||||
if (answers[questionId]) {
|
||||
answersToSave[questionId] = answers[questionId];
|
||||
}
|
||||
});
|
||||
|
||||
setSavingQuestions(new Set(unsavedQuestions));
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/questions/${projectId}/answers`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(answersToSave),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
setUnsavedQuestions(new Set());
|
||||
|
||||
const result = await response.json();
|
||||
setLastSaved(result.timestamp);
|
||||
|
||||
toast({
|
||||
title: "All Answers Saved",
|
||||
description: `Successfully saved ${Object.keys(answersToSave).length} answers.`,
|
||||
});
|
||||
} else {
|
||||
throw new Error(`Failed to save answers: ${response.statusText}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving all answers:', error);
|
||||
toast({
|
||||
title: "Save Error",
|
||||
description: "Failed to save your answers. Please try again.",
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setSavingQuestions(new Set());
|
||||
}
|
||||
};
|
||||
|
||||
// Export answers as CSV
|
||||
const handleExportAnswers = () => {
|
||||
if (!rfpDocument) return;
|
||||
|
||||
const rows = [
|
||||
['Section', 'Question', 'Answer'], // Header row
|
||||
];
|
||||
|
||||
rfpDocument.sections.forEach(section => {
|
||||
section.questions.forEach(question => {
|
||||
rows.push([
|
||||
section.title,
|
||||
question.question,
|
||||
answers[question.id]?.text || ''
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
const csvContent = rows.map(row =>
|
||||
row.map(cell =>
|
||||
typeof cell === 'string' ? `"${cell.replace(/"/g, '""')}"` : cell
|
||||
).join(',')
|
||||
).join('\n');
|
||||
|
||||
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.setAttribute('href', url);
|
||||
link.setAttribute('download', `${rfpDocument.documentName} - Answers.csv`);
|
||||
link.style.visibility = 'hidden';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
};
|
||||
|
||||
// Handle marking a question as complete
|
||||
const handleMarkComplete = (questionId: string) => {
|
||||
saveAnswer(questionId).then(() => {
|
||||
toast({
|
||||
title: "Question Completed",
|
||||
description: "This question has been marked as complete.",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Get the currently selected question data
|
||||
const getSelectedQuestionData = () => {
|
||||
if (!selectedQuestion || !rfpDocument) return null;
|
||||
|
||||
for (const section of rfpDocument.sections) {
|
||||
const question = section.questions.find(q => q.id === selectedQuestion);
|
||||
if (question) {
|
||||
return {
|
||||
question,
|
||||
section
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// Filter questions based on the search query and filter type
|
||||
const getFilteredQuestions = (filterType = "all") => {
|
||||
if (!rfpDocument) return [];
|
||||
|
||||
const allQuestions = rfpDocument.sections.flatMap(section => {
|
||||
return section.questions.map(question => ({
|
||||
...question,
|
||||
sectionTitle: section.title,
|
||||
sectionId: section.id
|
||||
}));
|
||||
});
|
||||
|
||||
let statusFiltered = allQuestions;
|
||||
|
||||
if (filterType === "answered") {
|
||||
statusFiltered = allQuestions.filter(q =>
|
||||
answers[q.id]?.text && answers[q.id].text.trim() !== ''
|
||||
);
|
||||
} else if (filterType === "unanswered") {
|
||||
statusFiltered = allQuestions.filter(q =>
|
||||
!answers[q.id]?.text || answers[q.id].text.trim() === ''
|
||||
);
|
||||
} else if (filterType === "flagged") {
|
||||
statusFiltered = allQuestions.filter(q => {
|
||||
const answer = answers[q.id]?.text || "";
|
||||
return answer && (
|
||||
answer.toLowerCase().includes("review") ||
|
||||
answer.toLowerCase().includes("incomplete") ||
|
||||
answer.toLowerCase().includes("todo")
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (!searchQuery) return statusFiltered;
|
||||
|
||||
const query = searchQuery.toLowerCase();
|
||||
return statusFiltered.filter(q =>
|
||||
q.question.toLowerCase().includes(query) ||
|
||||
q.sectionTitle.toLowerCase().includes(query)
|
||||
);
|
||||
};
|
||||
|
||||
// Count questions by status
|
||||
const getCounts = () => {
|
||||
if (!rfpDocument) return { all: 0, answered: 0, unanswered: 0, flagged: 0 };
|
||||
|
||||
const allQuestions = rfpDocument.sections.flatMap(s => s.questions);
|
||||
const answeredCount = allQuestions.filter(q => answers[q.id]?.text && answers[q.id].text.trim() !== '').length;
|
||||
const needsReviewCount = allQuestions.filter(q => {
|
||||
const answer = answers[q.id]?.text || "";
|
||||
return answer && (
|
||||
answer.toLowerCase().includes("review") ||
|
||||
answer.toLowerCase().includes("incomplete") ||
|
||||
answer.toLowerCase().includes("todo")
|
||||
);
|
||||
}).length;
|
||||
|
||||
return {
|
||||
all: allQuestions.length,
|
||||
answered: answeredCount,
|
||||
unanswered: allQuestions.length - answeredCount,
|
||||
flagged: needsReviewCount
|
||||
};
|
||||
};
|
||||
|
||||
// Handle source click to open the modal
|
||||
const handleSourceClick = (source: AnswerSource) => {
|
||||
setSelectedSource(source);
|
||||
setIsSourceModalOpen(true);
|
||||
};
|
||||
|
||||
const value: QuestionsContextType = {
|
||||
// UI state
|
||||
showAIPanel,
|
||||
setShowAIPanel,
|
||||
selectedQuestion,
|
||||
setSelectedQuestion,
|
||||
activeTab,
|
||||
setActiveTab,
|
||||
|
||||
// Data state
|
||||
isLoading,
|
||||
error,
|
||||
rfpDocument,
|
||||
project,
|
||||
answers,
|
||||
unsavedQuestions,
|
||||
|
||||
// Process state
|
||||
savingQuestions,
|
||||
lastSaved,
|
||||
isGenerating,
|
||||
searchQuery,
|
||||
setSearchQuery,
|
||||
selectedSource,
|
||||
setSelectedSource,
|
||||
isSourceModalOpen,
|
||||
setIsSourceModalOpen,
|
||||
selectedIndexes,
|
||||
setSelectedIndexes,
|
||||
availableIndexes,
|
||||
isLoadingIndexes,
|
||||
organizationConnected,
|
||||
|
||||
// Multi-step response state
|
||||
useMultiStep,
|
||||
setUseMultiStep,
|
||||
multiStepDialogOpen,
|
||||
setMultiStepDialogOpen,
|
||||
currentQuestionForMultiStep,
|
||||
currentQuestionText,
|
||||
|
||||
// Multi-step response hook
|
||||
generateMultiStepResponse,
|
||||
isMultiStepGenerating,
|
||||
multiStepSteps,
|
||||
multiStepFinalResponse,
|
||||
multiStepSources,
|
||||
resetMultiStepResponse,
|
||||
|
||||
// Action handlers
|
||||
handleAnswerChange,
|
||||
handleGenerateAnswer,
|
||||
saveAnswer,
|
||||
saveAllAnswers,
|
||||
handleExportAnswers,
|
||||
handleMarkComplete,
|
||||
handleSourceClick,
|
||||
handleIndexToggle,
|
||||
handleSelectAllIndexes,
|
||||
handleAcceptMultiStepResponse,
|
||||
handleCloseMultiStepDialog,
|
||||
|
||||
// Utility functions
|
||||
getFilteredQuestions,
|
||||
getCounts,
|
||||
getSelectedQuestionData,
|
||||
};
|
||||
|
||||
return (
|
||||
<QuestionsContext.Provider value={value}>
|
||||
{children}
|
||||
</QuestionsContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -1,623 +1,57 @@
|
||||
"use client"
|
||||
|
||||
import React, { useState, useEffect, Suspense, useMemo } from "react"
|
||||
import { useSearchParams } from "next/navigation"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { AlertCircle } from "lucide-react"
|
||||
import { Spinner } from "@/components/ui/spinner"
|
||||
import { toast } from "@/components/ui/use-toast"
|
||||
import React, { Suspense } from "react"
|
||||
import { Toaster } from "@/components/ui/toaster"
|
||||
import { RfpDocument, AnswerSource } from "@/types/api"
|
||||
import { useMultiStepResponse } from "@/hooks/use-multi-step-response"
|
||||
import { MultiStepResponseDialog } from "@/components/ui/multi-step-response-dialog"
|
||||
|
||||
// Import the new components
|
||||
// import { IndexSelector } from "./index-selector"
|
||||
import { QuestionsProvider, useQuestions } from "./questions-provider"
|
||||
import { QuestionsHeader } from "./questions-header"
|
||||
import { NoQuestionsAvailable } from "./no-questions-available"
|
||||
import { SourceDetailsDialog } from "./source-details-dialog"
|
||||
import { QuestionsTabsContent } from "./questions-tabs-content"
|
||||
|
||||
// Interfaces
|
||||
interface AnswerData {
|
||||
text: string;
|
||||
sources?: AnswerSource[];
|
||||
}
|
||||
|
||||
interface ProjectIndex {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface QuestionWithSection {
|
||||
id: string;
|
||||
question: string;
|
||||
sectionTitle: string;
|
||||
sectionId: string;
|
||||
}
|
||||
import { QuestionsFilterTabs } from "./questions-filter-tabs"
|
||||
import { QuestionsLoadingState, QuestionsErrorState } from "./questions-states"
|
||||
import { MultiStepResponseHandler } from "./multi-step-response-handler"
|
||||
|
||||
interface QuestionsSectionProps {
|
||||
projectId: string;
|
||||
}
|
||||
|
||||
|
||||
// Inner component that uses search params
|
||||
// Inner component that uses the context
|
||||
function QuestionsSectionInner({ projectId }: QuestionsSectionProps) {
|
||||
|
||||
// UI state
|
||||
const [showAIPanel, setShowAIPanel] = useState(false)
|
||||
const [selectedQuestion, setSelectedQuestion] = useState<string | null>(null)
|
||||
const [activeTab, setActiveTab] = useState("all")
|
||||
|
||||
// Data state
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [rfpDocument, setRfpDocument] = useState<RfpDocument | null>(null)
|
||||
const [project, setProject] = useState<any>(null)
|
||||
const [answers, setAnswers] = useState<Record<string, AnswerData>>({})
|
||||
const [unsavedQuestions, setUnsavedQuestions] = useState<Set<string>>(new Set())
|
||||
|
||||
// Process state
|
||||
const [savingQuestions, setSavingQuestions] = useState<Set<string>>(new Set())
|
||||
const [lastSaved, setLastSaved] = useState<string | null>(null)
|
||||
const [isGenerating, setIsGenerating] = useState<Record<string, boolean>>({})
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
const [selectedSource, setSelectedSource] = useState<AnswerSource | null>(null)
|
||||
const [isSourceModalOpen, setIsSourceModalOpen] = useState(false)
|
||||
const [selectedIndexes, setSelectedIndexes] = useState<Set<string>>(new Set())
|
||||
const [availableIndexes, setAvailableIndexes] = useState<ProjectIndex[]>([])
|
||||
const [isLoadingIndexes, setIsLoadingIndexes] = useState(false)
|
||||
const [organizationConnected, setOrganizationConnected] = useState(false)
|
||||
|
||||
// Multi-step response state
|
||||
const [useMultiStep, setUseMultiStep] = useState(false)
|
||||
const [multiStepDialogOpen, setMultiStepDialogOpen] = useState(false)
|
||||
const [currentQuestionForMultiStep, setCurrentQuestionForMultiStep] = useState<string | null>(null)
|
||||
const [currentQuestionText, setCurrentQuestionText] = useState<string>("")
|
||||
|
||||
// Use the new streaming multi-step response hook
|
||||
const {
|
||||
generateResponse: generateMultiStepResponse,
|
||||
isGenerating: isMultiStepGenerating,
|
||||
currentSteps: multiStepSteps,
|
||||
finalResponse: multiStepFinalResponse,
|
||||
sources: multiStepSources,
|
||||
reset: resetMultiStepResponse
|
||||
} = useMultiStepResponse({
|
||||
projectId: projectId || "",
|
||||
indexIds: Array.from(selectedIndexes),
|
||||
onComplete: (finalResponse, steps, sources) => {
|
||||
handleAcceptMultiStepResponse(finalResponse, sources);
|
||||
}
|
||||
})
|
||||
isLoading,
|
||||
error,
|
||||
rfpDocument,
|
||||
unsavedQuestions,
|
||||
savingQuestions,
|
||||
searchQuery,
|
||||
setSearchQuery,
|
||||
selectedSource,
|
||||
isSourceModalOpen,
|
||||
setIsSourceModalOpen,
|
||||
saveAllAnswers,
|
||||
handleExportAnswers,
|
||||
} = useQuestions();
|
||||
|
||||
// Load project data and questions when component mounts
|
||||
useEffect(() => {
|
||||
if (!projectId) {
|
||||
setError("No project ID provided");
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchProject = async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/projects/${projectId}`);
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to load project");
|
||||
}
|
||||
const data = await response.json();
|
||||
setProject(data);
|
||||
} catch (error) {
|
||||
console.error("Error loading project:", error);
|
||||
setError("Failed to load project. Please try again.");
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchIndexes = async () => {
|
||||
setIsLoadingIndexes(true);
|
||||
try {
|
||||
const response = await fetch(`/api/projects/${projectId}/indexes`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setOrganizationConnected(data.organizationConnected);
|
||||
if (data.organizationConnected) {
|
||||
const indexes = data.availableIndexes || [] as ProjectIndex[];
|
||||
setAvailableIndexes(indexes);
|
||||
|
||||
const currentIndexes = data.currentIndexes || [] as ProjectIndex[];
|
||||
const currentIndexIds = new Set(currentIndexes.map((index: ProjectIndex) => index.id)) as Set<string>;
|
||||
setSelectedIndexes(currentIndexIds);
|
||||
}
|
||||
} else {
|
||||
const errorData = await response.json().catch(() => ({ error: 'Unknown error' }));
|
||||
console.error("Error response from indexes API:", errorData);
|
||||
|
||||
if (errorData.error?.includes('Invalid index IDs')) {
|
||||
setSelectedIndexes(new Set());
|
||||
toast({
|
||||
title: "Index Sync Issue",
|
||||
description: "Some project indexes are out of sync. Please reconfigure your document indexes in project settings.",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
|
||||
setOrganizationConnected(true);
|
||||
setAvailableIndexes([]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading indexes:", error);
|
||||
setOrganizationConnected(false);
|
||||
setAvailableIndexes([]);
|
||||
setSelectedIndexes(new Set());
|
||||
} finally {
|
||||
setIsLoadingIndexes(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchQuestions = async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/questions/${projectId}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to load questions");
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
setRfpDocument(data);
|
||||
|
||||
const answersResponse = await fetch(`/api/questions/${projectId}/answers`);
|
||||
if (answersResponse.ok) {
|
||||
const savedAnswers = await answersResponse.json();
|
||||
|
||||
const normalizedAnswers: Record<string, AnswerData> = {};
|
||||
for (const [questionId, answerData] of Object.entries(savedAnswers)) {
|
||||
if (typeof answerData === 'string') {
|
||||
normalizedAnswers[questionId] = { text: answerData };
|
||||
} else {
|
||||
normalizedAnswers[questionId] = answerData as AnswerData;
|
||||
}
|
||||
}
|
||||
|
||||
setAnswers(normalizedAnswers);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading questions:", error);
|
||||
setError("Failed to load questions. Please try again.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
Promise.all([fetchProject(), fetchIndexes(), fetchQuestions()]).catch(error => {
|
||||
console.error("Error in parallel loading:", error);
|
||||
});
|
||||
}, [projectId]);
|
||||
|
||||
// Handle index selection
|
||||
const handleIndexToggle = (indexId: string) => {
|
||||
setSelectedIndexes(prev => {
|
||||
const newSelected = new Set(prev);
|
||||
if (newSelected.has(indexId)) {
|
||||
newSelected.delete(indexId);
|
||||
} else {
|
||||
newSelected.add(indexId);
|
||||
}
|
||||
return newSelected;
|
||||
});
|
||||
};
|
||||
|
||||
// Toggle all indexes
|
||||
const handleSelectAllIndexes = () => {
|
||||
if (selectedIndexes.size === availableIndexes.length) {
|
||||
setSelectedIndexes(new Set());
|
||||
} else {
|
||||
setSelectedIndexes(new Set(availableIndexes.map(index => index.id)));
|
||||
}
|
||||
};
|
||||
|
||||
// Handle answer changes
|
||||
const handleAnswerChange = (questionId: string, value: string) => {
|
||||
setAnswers(prev => {
|
||||
const existing = prev[questionId] || { text: '' };
|
||||
return {
|
||||
...prev,
|
||||
[questionId]: {
|
||||
...existing,
|
||||
text: value
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
setUnsavedQuestions(prev => {
|
||||
const updated = new Set(prev);
|
||||
updated.add(questionId);
|
||||
return updated;
|
||||
});
|
||||
};
|
||||
|
||||
// Modified generate answer handler to support multi-step
|
||||
const handleGenerateAnswer = async (questionId: string) => {
|
||||
const question = rfpDocument?.sections.flatMap(s => s.questions).find(q => q.id === questionId);
|
||||
|
||||
if (!question) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Question not found",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (useMultiStep) {
|
||||
setCurrentQuestionForMultiStep(questionId);
|
||||
setCurrentQuestionText(question.question);
|
||||
setMultiStepDialogOpen(true);
|
||||
resetMultiStepResponse();
|
||||
|
||||
if (!projectId) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Project ID not available",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await generateMultiStepResponse(question.question);
|
||||
} else {
|
||||
setIsGenerating(prev => ({ ...prev, [questionId]: true }));
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/generate-response', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
question: question.question,
|
||||
documentIds: project?.documentIds || [],
|
||||
selectedIndexIds: Array.from(selectedIndexes),
|
||||
useAllIndexes: false,
|
||||
projectId: project?.id
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to generate answer");
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
setAnswers(prev => ({
|
||||
...prev,
|
||||
[questionId]: {
|
||||
text: result.response,
|
||||
sources: result.sources
|
||||
}
|
||||
}));
|
||||
|
||||
setUnsavedQuestions(prev => {
|
||||
const updated = new Set(prev);
|
||||
updated.add(questionId);
|
||||
return updated;
|
||||
});
|
||||
|
||||
toast({
|
||||
title: "Answer Generated",
|
||||
description: "AI-generated answer has been created. Please review and save it.",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error generating answer:', error);
|
||||
toast({
|
||||
title: "Generation Error",
|
||||
description: "Failed to generate answer. Please try again.",
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setIsGenerating(prev => ({ ...prev, [questionId]: false }));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Handler for accepting multi-step response
|
||||
const handleAcceptMultiStepResponse = (response: string, sources: any[]) => {
|
||||
if (currentQuestionForMultiStep) {
|
||||
setAnswers(prev => ({
|
||||
...prev,
|
||||
[currentQuestionForMultiStep]: {
|
||||
text: response,
|
||||
sources: sources
|
||||
}
|
||||
}));
|
||||
|
||||
setUnsavedQuestions(prev => {
|
||||
const updated = new Set(prev);
|
||||
updated.add(currentQuestionForMultiStep);
|
||||
return updated;
|
||||
});
|
||||
|
||||
toast({
|
||||
title: "Multi-Step Answer Generated",
|
||||
description: "AI-generated answer with step-by-step reasoning has been created. Please review and save it.",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleCloseMultiStepDialog = () => {
|
||||
setMultiStepDialogOpen(false);
|
||||
setCurrentQuestionForMultiStep(null);
|
||||
resetMultiStepResponse();
|
||||
};
|
||||
|
||||
// Save a single answer
|
||||
const saveAnswer = async (questionId: string) => {
|
||||
if (!projectId || !answers[questionId]) return;
|
||||
|
||||
setSavingQuestions(prev => {
|
||||
const updated = new Set(prev);
|
||||
updated.add(questionId);
|
||||
return updated;
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/questions/${projectId}/answers/${questionId}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(answers[questionId]),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
setUnsavedQuestions(prev => {
|
||||
const updated = new Set(prev);
|
||||
updated.delete(questionId);
|
||||
return updated;
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
setLastSaved(result.timestamp);
|
||||
|
||||
toast({
|
||||
title: "Answer Saved",
|
||||
description: "Your answer has been saved successfully.",
|
||||
});
|
||||
} else {
|
||||
throw new Error(`Failed to save answer: ${response.statusText}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error saving answer for question ${questionId}:`, error);
|
||||
toast({
|
||||
title: "Save Error",
|
||||
description: "Failed to save your answer. Please try again.",
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setSavingQuestions(prev => {
|
||||
const updated = new Set(prev);
|
||||
updated.delete(questionId);
|
||||
return updated;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Save all unsaved answers
|
||||
const saveAllAnswers = async () => {
|
||||
if (!projectId || unsavedQuestions.size === 0) return;
|
||||
|
||||
const answersToSave: Record<string, AnswerData> = {};
|
||||
unsavedQuestions.forEach(questionId => {
|
||||
if (answers[questionId]) {
|
||||
answersToSave[questionId] = answers[questionId];
|
||||
}
|
||||
});
|
||||
|
||||
setSavingQuestions(new Set(unsavedQuestions));
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/questions/${projectId}/answers`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(answersToSave),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
setUnsavedQuestions(new Set());
|
||||
|
||||
const result = await response.json();
|
||||
setLastSaved(result.timestamp);
|
||||
|
||||
toast({
|
||||
title: "All Answers Saved",
|
||||
description: `Successfully saved ${Object.keys(answersToSave).length} answers.`,
|
||||
});
|
||||
} else {
|
||||
throw new Error(`Failed to save answers: ${response.statusText}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving all answers:', error);
|
||||
toast({
|
||||
title: "Save Error",
|
||||
description: "Failed to save your answers. Please try again.",
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setSavingQuestions(new Set());
|
||||
}
|
||||
};
|
||||
|
||||
// Export answers as CSV
|
||||
const handleExportAnswers = () => {
|
||||
if (!rfpDocument) return;
|
||||
|
||||
const rows = [
|
||||
['Section', 'Question', 'Answer'], // Header row
|
||||
];
|
||||
|
||||
rfpDocument.sections.forEach(section => {
|
||||
section.questions.forEach(question => {
|
||||
rows.push([
|
||||
section.title,
|
||||
question.question,
|
||||
answers[question.id]?.text || ''
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
const csvContent = rows.map(row =>
|
||||
row.map(cell =>
|
||||
typeof cell === 'string' ? `"${cell.replace(/"/g, '""')}"` : cell
|
||||
).join(',')
|
||||
).join('\n');
|
||||
|
||||
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.setAttribute('href', url);
|
||||
link.setAttribute('download', `${rfpDocument.documentName} - Answers.csv`);
|
||||
link.style.visibility = 'hidden';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
};
|
||||
|
||||
// Handle marking a question as complete
|
||||
const handleMarkComplete = (questionId: string) => {
|
||||
saveAnswer(questionId).then(() => {
|
||||
toast({
|
||||
title: "Question Completed",
|
||||
description: "This question has been marked as complete.",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Get the currently selected question data
|
||||
const getSelectedQuestionData = () => {
|
||||
if (!selectedQuestion || !rfpDocument) return null;
|
||||
|
||||
for (const section of rfpDocument.sections) {
|
||||
const question = section.questions.find(q => q.id === selectedQuestion);
|
||||
if (question) {
|
||||
return {
|
||||
question,
|
||||
section
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// Filter questions based on the search query and filter type
|
||||
const getFilteredQuestions = (filterType = "all") => {
|
||||
if (!rfpDocument) return [];
|
||||
|
||||
const allQuestions = rfpDocument.sections.flatMap(section => {
|
||||
return section.questions.map(question => ({
|
||||
...question,
|
||||
sectionTitle: section.title,
|
||||
sectionId: section.id
|
||||
}));
|
||||
});
|
||||
|
||||
let statusFiltered = allQuestions;
|
||||
|
||||
if (filterType === "answered") {
|
||||
statusFiltered = allQuestions.filter(q =>
|
||||
answers[q.id]?.text && answers[q.id].text.trim() !== ''
|
||||
);
|
||||
} else if (filterType === "unanswered") {
|
||||
statusFiltered = allQuestions.filter(q =>
|
||||
!answers[q.id]?.text || answers[q.id].text.trim() === ''
|
||||
);
|
||||
} else if (filterType === "flagged") {
|
||||
statusFiltered = allQuestions.filter(q => {
|
||||
const answer = answers[q.id]?.text || "";
|
||||
return answer && (
|
||||
answer.toLowerCase().includes("review") ||
|
||||
answer.toLowerCase().includes("incomplete") ||
|
||||
answer.toLowerCase().includes("todo")
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (!searchQuery) return statusFiltered;
|
||||
|
||||
const query = searchQuery.toLowerCase();
|
||||
return statusFiltered.filter(q =>
|
||||
q.question.toLowerCase().includes(query) ||
|
||||
q.sectionTitle.toLowerCase().includes(query)
|
||||
);
|
||||
};
|
||||
|
||||
// Count questions by status
|
||||
const getCounts = () => {
|
||||
if (!rfpDocument) return { all: 0, answered: 0, unanswered: 0, flagged: 0 };
|
||||
|
||||
const allQuestions = rfpDocument.sections.flatMap(s => s.questions);
|
||||
const answeredCount = allQuestions.filter(q => answers[q.id]?.text && answers[q.id].text.trim() !== '').length;
|
||||
const needsReviewCount = allQuestions.filter(q => {
|
||||
const answer = answers[q.id]?.text || "";
|
||||
return answer && (
|
||||
answer.toLowerCase().includes("review") ||
|
||||
answer.toLowerCase().includes("incomplete") ||
|
||||
answer.toLowerCase().includes("todo")
|
||||
);
|
||||
}).length;
|
||||
|
||||
return {
|
||||
all: allQuestions.length,
|
||||
answered: answeredCount,
|
||||
unanswered: allQuestions.length - answeredCount,
|
||||
flagged: needsReviewCount
|
||||
};
|
||||
};
|
||||
|
||||
// Handle source click to open the modal
|
||||
const handleSourceClick = (source: AnswerSource) => {
|
||||
setSelectedSource(source);
|
||||
setIsSourceModalOpen(true);
|
||||
};
|
||||
|
||||
// If still loading, show loading state
|
||||
// Loading state
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-[400px]">
|
||||
<div className="text-center">
|
||||
<Spinner size="lg" className="mb-4" />
|
||||
<p>Loading questions...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return <QuestionsLoadingState />;
|
||||
}
|
||||
|
||||
// If there was an error, show error state
|
||||
// Error state
|
||||
if (error) {
|
||||
return (
|
||||
<div className="p-8 text-center">
|
||||
<AlertCircle className="h-10 w-10 text-red-500 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium">Error Loading Questions</h3>
|
||||
<p className="text-muted-foreground mt-2">{error}</p>
|
||||
</div>
|
||||
);
|
||||
return <QuestionsErrorState error={error} />;
|
||||
}
|
||||
|
||||
// Check if there are no questions and show the NoQuestionsAvailable component
|
||||
|
||||
console.log("In QuestionsSectionInner, rfpDocument", rfpDocument);
|
||||
console.log("In QuestionsSectionInner, projectId", projectId);
|
||||
// No questions state
|
||||
if (!rfpDocument || rfpDocument.sections.length === 0 ||
|
||||
rfpDocument.sections.every(section => section.questions.length === 0)) {
|
||||
return (
|
||||
<>
|
||||
{projectId && <NoQuestionsAvailable projectId={projectId} />}
|
||||
</>
|
||||
);
|
||||
return <NoQuestionsAvailable projectId={projectId} />;
|
||||
}
|
||||
|
||||
const questionData = getSelectedQuestionData();
|
||||
const counts = getCounts();
|
||||
|
||||
return (
|
||||
<div className="space-y-6 p-12">
|
||||
<QuestionsHeader
|
||||
@@ -629,7 +63,7 @@ function QuestionsSectionInner({ projectId }: QuestionsSectionProps) {
|
||||
isSaving={savingQuestions.size > 0}
|
||||
/>
|
||||
|
||||
{/* Index Selection Panel */}
|
||||
{/* Index Selection Panel (commented out as in original) */}
|
||||
{/* <IndexSelector
|
||||
availableIndexes={availableIndexes}
|
||||
selectedIndexes={selectedIndexes}
|
||||
@@ -638,99 +72,44 @@ function QuestionsSectionInner({ projectId }: QuestionsSectionProps) {
|
||||
onSelectAllIndexes={handleSelectAllIndexes}
|
||||
/> */}
|
||||
|
||||
{/* Source Details Dialog */}
|
||||
<SourceDetailsDialog
|
||||
isOpen={isSourceModalOpen}
|
||||
onClose={() => setIsSourceModalOpen(false)}
|
||||
source={selectedSource}
|
||||
/>
|
||||
|
||||
<Tabs defaultValue="all" value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList className="grid w-full grid-cols-4 mb-4">
|
||||
<TabsTrigger value="all" className="gap-1">
|
||||
All Questions
|
||||
<Badge variant="secondary" className="ml-1">{counts.all}</Badge>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="answered" className="gap-1">
|
||||
Answered
|
||||
<Badge variant="secondary" className="ml-1">{counts.answered}</Badge>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="unanswered" className="gap-1">
|
||||
Unanswered
|
||||
<Badge variant="secondary" className="ml-1">{counts.unanswered}</Badge>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="flagged" className="gap-1">
|
||||
Needs Review
|
||||
<Badge variant="secondary" className="ml-1">{counts.flagged}</Badge>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{["all", "answered", "unanswered", "flagged"].map(filterType => (
|
||||
<TabsContent key={filterType} value={filterType} className="space-y-4">
|
||||
<QuestionsTabsContent
|
||||
questions={getFilteredQuestions(filterType)}
|
||||
selectedQuestion={selectedQuestion}
|
||||
questionData={questionData}
|
||||
answers={answers}
|
||||
unsavedQuestions={unsavedQuestions}
|
||||
selectedIndexes={selectedIndexes}
|
||||
isGenerating={isGenerating}
|
||||
isMultiStepGenerating={isMultiStepGenerating}
|
||||
savingQuestions={savingQuestions}
|
||||
useMultiStep={useMultiStep}
|
||||
showAIPanel={showAIPanel}
|
||||
filterType={filterType}
|
||||
onSelectQuestion={(id) => {
|
||||
setSelectedQuestion(id);
|
||||
setShowAIPanel(false);
|
||||
}}
|
||||
onAnswerChange={handleAnswerChange}
|
||||
onSave={saveAnswer}
|
||||
onMarkComplete={handleMarkComplete}
|
||||
onGenerateAnswer={handleGenerateAnswer}
|
||||
onSourceClick={handleSourceClick}
|
||||
onMultiStepToggle={setUseMultiStep}
|
||||
rfpDocument={rfpDocument}
|
||||
searchQuery={searchQuery}
|
||||
/>
|
||||
</TabsContent>
|
||||
))}
|
||||
</Tabs>
|
||||
{/* Questions Filter Tabs */}
|
||||
<QuestionsFilterTabs rfpDocument={rfpDocument} />
|
||||
|
||||
{/* Multi-step response dialog */}
|
||||
<MultiStepResponseDialog
|
||||
isOpen={multiStepDialogOpen}
|
||||
onClose={handleCloseMultiStepDialog}
|
||||
questionText={currentQuestionText}
|
||||
isGenerating={isMultiStepGenerating}
|
||||
currentSteps={multiStepSteps}
|
||||
finalResponse={multiStepFinalResponse}
|
||||
sources={multiStepSources}
|
||||
onAcceptResponse={handleAcceptMultiStepResponse}
|
||||
/>
|
||||
{/* Multi-step Response Dialog */}
|
||||
<MultiStepResponseHandler />
|
||||
|
||||
<Toaster />
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Main export that wraps the inner component with Suspense
|
||||
// Main export that wraps the inner component with Suspense and Provider
|
||||
export function QuestionsSection({ projectId }: QuestionsSectionProps) {
|
||||
return (
|
||||
<Suspense fallback={
|
||||
<div className="space-y-6 p-12">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="h-8 w-36 bg-muted animate-pulse rounded"></div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-9 w-64 bg-muted animate-pulse rounded"></div>
|
||||
<div className="h-9 w-24 bg-muted animate-pulse rounded"></div>
|
||||
<div className="h-9 w-32 bg-muted animate-pulse rounded"></div>
|
||||
<QuestionsProvider projectId={projectId}>
|
||||
<Suspense fallback={
|
||||
<div className="space-y-6 p-12">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="h-8 w-36 bg-muted animate-pulse rounded"></div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-9 w-64 bg-muted animate-pulse rounded"></div>
|
||||
<div className="h-9 w-24 bg-muted animate-pulse rounded"></div>
|
||||
<div className="h-9 w-32 bg-muted animate-pulse rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-12 bg-muted animate-pulse rounded"></div>
|
||||
<div className="h-[500px] bg-muted animate-pulse rounded"></div>
|
||||
</div>
|
||||
<div className="h-12 bg-muted animate-pulse rounded"></div>
|
||||
<div className="h-[500px] bg-muted animate-pulse rounded"></div>
|
||||
</div>
|
||||
}>
|
||||
<QuestionsSectionInner projectId={projectId} />
|
||||
</Suspense>
|
||||
)
|
||||
}>
|
||||
<QuestionsSectionInner projectId={projectId} />
|
||||
</Suspense>
|
||||
</QuestionsProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
"use client"
|
||||
|
||||
import React from "react"
|
||||
import { AlertCircle } from "lucide-react"
|
||||
import { Spinner } from "@/components/ui/spinner"
|
||||
|
||||
export function QuestionsLoadingState() {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-[400px]">
|
||||
<div className="text-center">
|
||||
<Spinner size="lg" className="mb-4" />
|
||||
<p>Loading questions...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface QuestionsErrorStateProps {
|
||||
error: string;
|
||||
}
|
||||
|
||||
export function QuestionsErrorState({ error }: QuestionsErrorStateProps) {
|
||||
return (
|
||||
<div className="p-8 text-center">
|
||||
<AlertCircle className="h-10 w-10 text-red-500 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium">Error Loading Questions</h3>
|
||||
<p className="text-muted-foreground mt-2">{error}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function QuestionsSkeletonLoader() {
|
||||
return (
|
||||
<div className="space-y-6 p-12">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="h-8 w-36 bg-muted animate-pulse rounded"></div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-9 w-64 bg-muted animate-pulse rounded"></div>
|
||||
<div className="h-9 w-24 bg-muted animate-pulse rounded"></div>
|
||||
<div className="h-9 w-32 bg-muted animate-pulse rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-12 bg-muted animate-pulse rounded"></div>
|
||||
<div className="h-[500px] bg-muted animate-pulse rounded"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import React, { useState, useEffect, Suspense } from "react"
|
||||
import React, { useState, useEffect, Suspense, use } from "react"
|
||||
import { useRouter, useSearchParams } from "next/navigation"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from "@/components/ui/card"
|
||||
@@ -23,10 +23,8 @@ type Question = {
|
||||
question: string;
|
||||
}
|
||||
|
||||
function CreateQuestionsPageInner() {
|
||||
function CreateQuestionsPageInner( { projectId }: { projectId: string } ) {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const projectId = searchParams.get("projectId");
|
||||
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
@@ -192,7 +190,7 @@ function CreateQuestionsPageInner() {
|
||||
});
|
||||
|
||||
// Redirect to questions page
|
||||
router.push(`/questions?projectId=${projectId}`);
|
||||
router.push(`/projects/${projectId}/questions`);
|
||||
} catch (error) {
|
||||
console.error("Error saving questions:", error);
|
||||
toast({
|
||||
@@ -207,7 +205,7 @@ function CreateQuestionsPageInner() {
|
||||
|
||||
// Back to questions
|
||||
const goBack = () => {
|
||||
router.push(`/questions?projectId=${projectId}`);
|
||||
router.push(`/projects/${projectId}/questions`);
|
||||
};
|
||||
|
||||
// Loading state
|
||||
@@ -344,7 +342,9 @@ function CreateQuestionsPageInner() {
|
||||
}
|
||||
|
||||
// Main export that wraps the inner component with Suspense
|
||||
export default function CreateQuestionsPage() {
|
||||
export default function CreateQuestionsPage( { params }: { params: Promise<{ projectId: string }> } ) {
|
||||
const { projectId } = use(params);
|
||||
|
||||
return (
|
||||
<Suspense fallback={
|
||||
<div className="min-h-screen bg-background">
|
||||
@@ -356,7 +356,7 @@ export default function CreateQuestionsPage() {
|
||||
</div>
|
||||
</div>
|
||||
}>
|
||||
<CreateQuestionsPageInner />
|
||||
<CreateQuestionsPageInner projectId={projectId} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
+1
-1
@@ -52,7 +52,7 @@ function UploadPageInner() {
|
||||
|
||||
// Handle view questions button click
|
||||
const handleViewQuestions = (projectId: string) => {
|
||||
router.push(`/questions?projectId=${projectId}`);
|
||||
router.push(`/projects/${projectId}/questions`);
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
|
||||
@@ -219,7 +219,7 @@ export function FileUploader({
|
||||
} else {
|
||||
// This path shouldn't typically be used, but just in case
|
||||
setShowProcessingModal(false);
|
||||
router.push(`/questions?projectId=${result.documentId}`);
|
||||
router.push(`/projects/${result.documentId}/questions`);
|
||||
}
|
||||
}
|
||||
}, 200);
|
||||
|
||||
@@ -0,0 +1,271 @@
|
||||
'use client';
|
||||
|
||||
import React, { Suspense, useState, useEffect } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { usePathname, useParams, useRouter } from 'next/navigation';
|
||||
import { ChevronRight, LogOut, Settings, Users, FileText, HelpCircle, Building2 } from 'lucide-react';
|
||||
import Image from 'next/image';
|
||||
import { logout } from '@/app/login/actions';
|
||||
import { useTransition } from 'react';
|
||||
import { getCurrentUserEmail } from '@/app/user/actions';
|
||||
import { useOrganization } from '@/context/organization-context';
|
||||
|
||||
interface BreadcrumbItem {
|
||||
label: string;
|
||||
href?: string;
|
||||
icon?: React.ReactNode;
|
||||
active?: boolean;
|
||||
}
|
||||
|
||||
function GlobalHeaderContent() {
|
||||
const pathname = usePathname();
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [userEmail, setUserEmail] = useState<string | null>(null);
|
||||
const { currentOrganization, currentProject } = useOrganization();
|
||||
|
||||
// Fetch user email on component mount
|
||||
useEffect(() => {
|
||||
const fetchUserEmail = async () => {
|
||||
const email = await getCurrentUserEmail();
|
||||
setUserEmail(email);
|
||||
};
|
||||
fetchUserEmail();
|
||||
}, []);
|
||||
|
||||
// Build dynamic breadcrumbs based on current route
|
||||
const buildBreadcrumbs = (): BreadcrumbItem[] => {
|
||||
const breadcrumbs: BreadcrumbItem[] = [];
|
||||
|
||||
// Home/Organizations
|
||||
if (pathname === '/organizations') {
|
||||
breadcrumbs.push({
|
||||
label: 'Organizations',
|
||||
href: '/organizations',
|
||||
icon: <Building2 className="h-4 w-4" />,
|
||||
active: true
|
||||
});
|
||||
return breadcrumbs;
|
||||
}
|
||||
|
||||
// Organization context
|
||||
if (currentOrganization) {
|
||||
breadcrumbs.push({
|
||||
label: currentOrganization.name,
|
||||
href: `/organizations/${currentOrganization.id}`,
|
||||
icon: <Building2 className="h-4 w-4" />
|
||||
});
|
||||
|
||||
// Project context
|
||||
if (currentProject) {
|
||||
breadcrumbs.push({
|
||||
label: currentProject.name,
|
||||
href: `/projects/${currentProject.id}`
|
||||
});
|
||||
|
||||
// Project sub-pages
|
||||
if (pathname.includes('/documents')) {
|
||||
breadcrumbs.push({
|
||||
label: 'Documents',
|
||||
href: `/projects/${currentProject.id}/documents`,
|
||||
icon: <FileText className="h-4 w-4" />,
|
||||
active: true
|
||||
});
|
||||
} else if (pathname.includes('/questions')) {
|
||||
breadcrumbs.push({
|
||||
label: 'Questions',
|
||||
href: `/projects/${currentProject.id}/questions`,
|
||||
icon: <HelpCircle className="h-4 w-4" />,
|
||||
active: true
|
||||
});
|
||||
} else if (pathname.includes('/team')) {
|
||||
breadcrumbs.push({
|
||||
label: 'Team',
|
||||
href: `/projects/${currentProject.id}/team`,
|
||||
icon: <Users className="h-4 w-4" />,
|
||||
active: true
|
||||
});
|
||||
} else {
|
||||
// Default project page
|
||||
breadcrumbs[breadcrumbs.length - 1].active = true;
|
||||
}
|
||||
} else {
|
||||
// Organization sub-pages
|
||||
if (pathname.includes('/team')) {
|
||||
breadcrumbs.push({
|
||||
label: 'Team',
|
||||
icon: <Users className="h-4 w-4" />,
|
||||
active: true
|
||||
});
|
||||
} else if (pathname.includes('/settings')) {
|
||||
breadcrumbs.push({
|
||||
label: 'Settings',
|
||||
icon: <Settings className="h-4 w-4" />,
|
||||
active: true
|
||||
});
|
||||
} else if (pathname.includes('/documents')) {
|
||||
breadcrumbs.push({
|
||||
label: 'Documents',
|
||||
icon: <FileText className="h-4 w-4" />,
|
||||
active: true
|
||||
});
|
||||
} else {
|
||||
// Default organization page
|
||||
breadcrumbs[breadcrumbs.length - 1].active = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return breadcrumbs;
|
||||
};
|
||||
|
||||
const breadcrumbs = buildBreadcrumbs();
|
||||
|
||||
// Don't show header on certain pages
|
||||
if (pathname === '/' || pathname === '/login' || pathname === '/signup') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="border-b bg-background">
|
||||
|
||||
|
||||
{/* Main Header */}
|
||||
<header className="bg-background">
|
||||
<div className="container mx-auto flex h-12 items-center justify-between px-4">
|
||||
{/* Left side - Logo and Breadcrumbs */}
|
||||
<div className="flex items-center gap-3">
|
||||
<Link href="/organizations" className="flex items-center gap-2">
|
||||
<Image src="/llamaindex_logo.jpeg" alt="AutoRFP" width={24} height={24} />
|
||||
<span className="font-semibold text-lg">AutoRFP</span>
|
||||
</Link>
|
||||
|
||||
{/* Breadcrumbs */}
|
||||
{breadcrumbs.length > 0 && (
|
||||
<nav className="flex items-center gap-1 text-sm">
|
||||
<ChevronRight className="h-4 w-4 text-muted-foreground" />
|
||||
{breadcrumbs.map((crumb, index) => (
|
||||
<React.Fragment key={index}>
|
||||
{crumb.href ? (
|
||||
<Link
|
||||
href={crumb.href}
|
||||
className={`flex items-center gap-1.5 px-2 py-1 rounded-md hover:bg-muted transition-colors ${
|
||||
crumb.active ? 'text-foreground font-medium' : 'text-muted-foreground hover:text-foreground'
|
||||
}`}
|
||||
>
|
||||
{crumb.icon}
|
||||
{crumb.label}
|
||||
</Link>
|
||||
) : (
|
||||
<span className={`flex items-center gap-1.5 px-2 py-1 ${
|
||||
crumb.active ? 'text-foreground font-medium' : 'text-muted-foreground'
|
||||
}`}>
|
||||
{crumb.icon}
|
||||
{crumb.label}
|
||||
</span>
|
||||
)}
|
||||
{index < breadcrumbs.length - 1 && (
|
||||
<ChevronRight className="h-3 w-3 text-muted-foreground" />
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</nav>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Right side - User menu */}
|
||||
<div className="flex items-center gap-3">
|
||||
<Button variant="ghost" size="sm">
|
||||
<HelpCircle className="h-4 w-4 mr-1.5" />
|
||||
Help
|
||||
</Button>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="sm" className="flex items-center gap-2">
|
||||
<Avatar className="h-6 w-6">
|
||||
<AvatarFallback className="text-xs">
|
||||
{userEmail ? userEmail.charAt(0).toUpperCase() : 'U'}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<span className="text-sm">{userEmail?.split('@')[0] || 'User'}</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-56">
|
||||
<DropdownMenuLabel>
|
||||
<div className="flex flex-col space-y-1">
|
||||
<p className="text-sm font-medium leading-none">My Account</p>
|
||||
{userEmail && (
|
||||
<p className="text-xs leading-none text-muted-foreground">
|
||||
{userEmail}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => router.push('/organizations')}>
|
||||
<Building2 className="mr-2 h-4 w-4" />
|
||||
<span>Organizations</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<Settings className="mr-2 h-4 w-4" />
|
||||
<span>Account Settings</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
className="text-destructive cursor-pointer"
|
||||
onClick={() => {
|
||||
startTransition(async () => {
|
||||
await logout();
|
||||
});
|
||||
}}
|
||||
disabled={isPending}
|
||||
>
|
||||
<LogOut className="mr-2 h-4 w-4" />
|
||||
<span>{isPending ? 'Logging out...' : 'Log out'}</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function GlobalHeader() {
|
||||
return (
|
||||
<Suspense fallback={
|
||||
<div className="border-b bg-background">
|
||||
<div className="bg-green-100 border-green-200 border-b px-4 py-2">
|
||||
<div className="h-6 animate-pulse bg-green-200 rounded w-48"></div>
|
||||
</div>
|
||||
<header className="bg-background">
|
||||
<div className="container mx-auto flex h-12 items-center justify-between px-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="h-6 w-32 animate-pulse bg-muted rounded"></div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="h-6 w-16 animate-pulse bg-muted rounded"></div>
|
||||
<div className="h-6 w-6 animate-pulse bg-muted rounded-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
}>
|
||||
<GlobalHeaderContent />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
@@ -31,6 +31,7 @@ function HeaderContent() {
|
||||
// Check if we're on different page types
|
||||
const isOrgPage = pathname.startsWith('/org/');
|
||||
const isProjectPage = pathname.startsWith('/project');
|
||||
const isOrganizationsPage = pathname === '/organizations';
|
||||
const showOnHomePage = pathname === '/' || pathname === '/new-organization';
|
||||
|
||||
// Get current project and org IDs from URL params
|
||||
@@ -46,8 +47,8 @@ function HeaderContent() {
|
||||
fetchUserEmail();
|
||||
}, []);
|
||||
|
||||
// Don't show if we're not on the home page, org page, or project page
|
||||
if (!showOnHomePage && !isOrgPage && !isProjectPage) {
|
||||
// Don't show if we're not on the home page, org page, organizations page, or project page
|
||||
if (!showOnHomePage && !isOrgPage && !isProjectPage && !isOrganizationsPage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -229,7 +229,7 @@ function Sidebar({
|
||||
<div
|
||||
data-slot="sidebar-container"
|
||||
className={cn(
|
||||
"fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex",
|
||||
"inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex",
|
||||
side === "left"
|
||||
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
|
||||
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
|
||||
@@ -310,7 +310,7 @@ function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
|
||||
data-slot="sidebar-inset"
|
||||
className={cn(
|
||||
"bg-background relative flex w-full flex-1 flex-col",
|
||||
"md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2",
|
||||
"md:peer-data-[variant=inset]:m-0 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset] md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -643,7 +643,7 @@ function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
|
||||
data-slot="sidebar-menu-sub"
|
||||
data-sidebar="menu-sub"
|
||||
className={cn(
|
||||
"border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5",
|
||||
"border-sidebar-border flex min-w-0 translate-x-px flex-col gap-1 px-2.5 py-0.5",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className
|
||||
)}
|
||||
|
||||
@@ -70,7 +70,7 @@ export function UploadComponent({ projectId }: UploadComponentProps) {
|
||||
// Wait a moment to show completion state before redirecting
|
||||
setTimeout(() => {
|
||||
// Redirect to questions page
|
||||
router.push(`/questions?projectId=${projectId}`);
|
||||
router.push(`/projects/${projectId}/questions`);
|
||||
}, 1000);
|
||||
} catch (error) {
|
||||
console.error('Error processing document:', error);
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
import { UserSection } from "@/components/user-section";
|
||||
import { OrganizationProjectSwitcher } from "@/components/organization-project-switcher";
|
||||
import { OrganizationProvider, useOrganization } from "@/context/organization-context";
|
||||
import { useOrganization } from "@/context/organization-context";
|
||||
import {
|
||||
BarChart3,
|
||||
ChevronRight,
|
||||
@@ -135,21 +135,6 @@ function AppSidebar() {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Tools",
|
||||
items: [
|
||||
{
|
||||
title: "Upload Documents",
|
||||
url: `/upload?projectId=${projectId}`,
|
||||
icon: Upload,
|
||||
},
|
||||
{
|
||||
title: "Create Questions",
|
||||
url: `/questions/create?projectId=${projectId}`,
|
||||
icon: Plus,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// Get navigation items based on current context
|
||||
@@ -168,29 +153,23 @@ function AppSidebar() {
|
||||
const contextNavigationItems = getNavigationItems();
|
||||
|
||||
return (
|
||||
<Sidebar variant="inset" className="border-r">
|
||||
<Sidebar variant="inset" collapsible="icon" className="border-r h-full">
|
||||
<SidebarHeader>
|
||||
<OrganizationProjectSwitcher />
|
||||
</SidebarHeader>
|
||||
|
||||
<SidebarContent>
|
||||
<SidebarContent className="overflow-y-auto">
|
||||
<SidebarMenu>
|
||||
{/* Context-specific navigation (organization or project) */}
|
||||
{contextNavigationItems.map((group) => (
|
||||
<div key={group.title}>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton className="text-xs font-medium text-muted-foreground">
|
||||
{group.title}
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuSub>
|
||||
{group.items.map((item) => (
|
||||
<SidebarMenuSubItem key={item.title}>
|
||||
<SidebarMenuSubButton
|
||||
asChild
|
||||
isActive={
|
||||
pathname === item.url ||
|
||||
pathname.startsWith(item.url) ||
|
||||
pathname === item.url ||
|
||||
(item.url.includes('?') && pathname === item.url.split('?')[0] &&
|
||||
typeof window !== 'undefined' && window.location.search.includes(item.url.split('?')[1]))
|
||||
}
|
||||
@@ -247,21 +226,26 @@ interface SidebarLayoutProps {
|
||||
export function SidebarLayout({ children }: SidebarLayoutProps) {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<OrganizationProvider>
|
||||
<SidebarProvider>
|
||||
<AppSidebar />
|
||||
<SidebarInset>
|
||||
<header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12">
|
||||
<div className="flex items-center gap-2 px-4">
|
||||
<SidebarTrigger className="-ml-1" />
|
||||
</div>
|
||||
</header>
|
||||
<div className="flex flex-1 flex-col gap-4 p-4 pt-0">
|
||||
{children}
|
||||
<SidebarProvider>
|
||||
|
||||
<AppSidebar />
|
||||
|
||||
{/* Main content area with independent scrolling */}
|
||||
<SidebarInset className="flex-1 flex flex-col overflow-hidden">
|
||||
{/* Fixed header */}
|
||||
<header className="flex h-16 shrink-0 items-center border-b bg-background transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12">
|
||||
<div className="flex items-center gap-2 px-4">
|
||||
<SidebarTrigger className="-ml-1" />
|
||||
</div>
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
</OrganizationProvider>
|
||||
</header>
|
||||
|
||||
{/* Scrollable content area */}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{children}
|
||||
</div>
|
||||
</SidebarInset>
|
||||
|
||||
</SidebarProvider>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
@@ -171,6 +171,7 @@ export const projectService = {
|
||||
},
|
||||
|
||||
async getQuestions(projectId: string) {
|
||||
console.log("In getQuestions, projectId", projectId);
|
||||
console.log(`Fetching questions for project ${projectId}`);
|
||||
|
||||
try {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { ThemeProvider } from "next-themes";
|
||||
import { ReactNode } from "react";
|
||||
import { OrganizationProvider } from "@/context/organization-context";
|
||||
|
||||
export function Providers({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
@@ -11,7 +12,9 @@ export function Providers({ children }: { children: ReactNode }) {
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
{children}
|
||||
<OrganizationProvider>
|
||||
{children}
|
||||
</OrganizationProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user