Merge pull request #20 from run-llama/frontend_improvement_continued

Frontend improvement continued
This commit is contained in:
zli484
2025-07-16 23:03:39 -07:00
committed by GitHub
22 changed files with 1373 additions and 883 deletions
-29
View File
@@ -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
View File
@@ -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
View File
@@ -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 -1
View File
@@ -5,5 +5,9 @@ export default function OrganizationsLayout({
}: {
children: React.ReactNode;
}) {
return <SidebarLayout>{children}</SidebarLayout>;
return (
<SidebarLayout>{children}</SidebarLayout>
);
}
+1 -1
View File
@@ -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
View File
@@ -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) {
+1 -1
View File
@@ -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);
+271
View File
@@ -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>
);
}
+3 -2
View File
@@ -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;
}
+3 -3
View File
@@ -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
)}
+1 -1
View File
@@ -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);
+23 -39
View File
@@ -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>
);
}
+1
View File
@@ -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 {
+4 -1
View File
@@ -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>
);
}