agent-starter-pack 0.11.2__py3-none-any.whl → 0.12.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. {agent_starter_pack-0.11.2.dist-info → agent_starter_pack-0.12.0.dist-info}/METADATA +1 -1
  2. {agent_starter_pack-0.11.2.dist-info → agent_starter_pack-0.12.0.dist-info}/RECORD +41 -66
  3. agents/adk_base/app/__init__.py +17 -0
  4. agents/adk_base/notebooks/adk_app_testing.ipynb +4 -1
  5. agents/adk_base/tests/integration/test_agent.py +1 -1
  6. agents/agentic_rag/app/__init__.py +17 -0
  7. agents/agentic_rag/app/agent.py +2 -2
  8. agents/agentic_rag/notebooks/adk_app_testing.ipynb +4 -1
  9. agents/agentic_rag/tests/integration/test_agent.py +2 -2
  10. agents/crewai_coding_crew/tests/integration/test_agent.py +1 -1
  11. agents/langgraph_base_react/tests/integration/test_agent.py +1 -1
  12. agents/live_api/tests/unit/test_server.py +6 -6
  13. llm.txt +15 -4
  14. src/base_template/Makefile +5 -5
  15. src/base_template/README.md +4 -4
  16. src/base_template/deployment/terraform/{% if cookiecutter.cicd_runner == 'google_cloud_build' %}build_triggers.tf{% else %}unused_build_triggers.tf{% endif %} +2 -2
  17. src/base_template/pyproject.toml +2 -2
  18. src/base_template/{% if cookiecutter.cicd_runner == 'github_actions' %}.github{% else %}unused_github{% endif %}/workflows/deploy-to-prod.yaml +1 -1
  19. src/base_template/{% if cookiecutter.cicd_runner == 'github_actions' %}.github{% else %}unused_github{% endif %}/workflows/pr_checks.yaml +1 -1
  20. src/base_template/{% if cookiecutter.cicd_runner == 'github_actions' %}.github{% else %}unused_github{% endif %}/workflows/staging.yaml +2 -2
  21. src/base_template/{% if cookiecutter.cicd_runner == 'google_cloud_build' %}.cloudbuild{% else %}unused_.cloudbuild{% endif %}/deploy-to-prod.yaml +1 -1
  22. src/base_template/{% if cookiecutter.cicd_runner == 'google_cloud_build' %}.cloudbuild{% else %}unused_.cloudbuild{% endif %}/staging.yaml +1 -1
  23. src/cli/commands/create.py +25 -2
  24. src/cli/commands/enhance.py +94 -15
  25. src/cli/commands/list.py +1 -1
  26. src/cli/utils/remote_template.py +1 -1
  27. src/cli/utils/template.py +120 -41
  28. src/deployment_targets/agent_engine/tests/integration/test_agent_engine_app.py +3 -3
  29. src/deployment_targets/agent_engine/{app → {{cookiecutter.agent_directory}}}/agent_engine_app.py +10 -10
  30. src/deployment_targets/cloud_run/Dockerfile +2 -2
  31. src/deployment_targets/cloud_run/tests/integration/test_server_e2e.py +3 -3
  32. src/deployment_targets/cloud_run/tests/load_test/README.md +1 -1
  33. src/deployment_targets/cloud_run/tests/load_test/load_test.py +2 -2
  34. {agents/live_api/app → src/deployment_targets/cloud_run/{{cookiecutter.agent_directory}}}/server.py +186 -7
  35. src/resources/docs/adk-cheatsheet.md +3 -3
  36. src/base_template/app/__init__.py +0 -3
  37. src/deployment_targets/cloud_run/app/server.py +0 -206
  38. src/frontends/adk_gemini_fullstack/frontend/components.json +0 -21
  39. src/frontends/adk_gemini_fullstack/frontend/eslint.config.js +0 -28
  40. src/frontends/adk_gemini_fullstack/frontend/index.html +0 -12
  41. src/frontends/adk_gemini_fullstack/frontend/package-lock.json +0 -6105
  42. src/frontends/adk_gemini_fullstack/frontend/package.json +0 -47
  43. src/frontends/adk_gemini_fullstack/frontend/src/App.tsx +0 -564
  44. src/frontends/adk_gemini_fullstack/frontend/src/components/ActivityTimeline.tsx +0 -244
  45. src/frontends/adk_gemini_fullstack/frontend/src/components/ChatMessagesView.tsx +0 -420
  46. src/frontends/adk_gemini_fullstack/frontend/src/components/InputForm.tsx +0 -60
  47. src/frontends/adk_gemini_fullstack/frontend/src/components/WelcomeScreen.tsx +0 -56
  48. src/frontends/adk_gemini_fullstack/frontend/src/components/ui/badge.tsx +0 -46
  49. src/frontends/adk_gemini_fullstack/frontend/src/components/ui/button.tsx +0 -59
  50. src/frontends/adk_gemini_fullstack/frontend/src/components/ui/card.tsx +0 -92
  51. src/frontends/adk_gemini_fullstack/frontend/src/components/ui/input.tsx +0 -21
  52. src/frontends/adk_gemini_fullstack/frontend/src/components/ui/scroll-area.tsx +0 -56
  53. src/frontends/adk_gemini_fullstack/frontend/src/components/ui/select.tsx +0 -183
  54. src/frontends/adk_gemini_fullstack/frontend/src/components/ui/tabs.tsx +0 -64
  55. src/frontends/adk_gemini_fullstack/frontend/src/components/ui/textarea.tsx +0 -18
  56. src/frontends/adk_gemini_fullstack/frontend/src/global.css +0 -154
  57. src/frontends/adk_gemini_fullstack/frontend/src/main.tsx +0 -13
  58. src/frontends/adk_gemini_fullstack/frontend/src/utils.ts +0 -7
  59. src/frontends/adk_gemini_fullstack/frontend/src/vite-env.d.ts +0 -1
  60. src/frontends/adk_gemini_fullstack/frontend/tsconfig.json +0 -28
  61. src/frontends/adk_gemini_fullstack/frontend/tsconfig.node.json +0 -24
  62. src/frontends/adk_gemini_fullstack/frontend/vite.config.ts +0 -41
  63. {agent_starter_pack-0.11.2.dist-info → agent_starter_pack-0.12.0.dist-info}/WHEEL +0 -0
  64. {agent_starter_pack-0.11.2.dist-info → agent_starter_pack-0.12.0.dist-info}/entry_points.txt +0 -0
  65. {agent_starter_pack-0.11.2.dist-info → agent_starter_pack-0.12.0.dist-info}/licenses/LICENSE +0 -0
  66. /src/base_template/{app → {{cookiecutter.agent_directory}}}/utils/gcs.py +0 -0
  67. /src/base_template/{app → {{cookiecutter.agent_directory}}}/utils/tracing.py +0 -0
  68. /src/base_template/{app → {{cookiecutter.agent_directory}}}/utils/typing.py +0 -0
@@ -1,244 +0,0 @@
1
- import {
2
- Card,
3
- CardContent,
4
- CardDescription,
5
- CardHeader,
6
- } from "@/components/ui/card";
7
- import { ScrollArea } from "@/components/ui/scroll-area";
8
- import {
9
- Loader2,
10
- Activity,
11
- Info,
12
- Search,
13
- TextSearch,
14
- Brain,
15
- Pen,
16
- ChevronDown,
17
- ChevronUp,
18
- Link,
19
- } from "lucide-react";
20
- import { useEffect, useState } from "react";
21
- import ReactMarkdown from "react-markdown";
22
-
23
- export interface ProcessedEvent {
24
- title: string;
25
- data: any;
26
- }
27
-
28
- interface ActivityTimelineProps {
29
- processedEvents: ProcessedEvent[];
30
- isLoading: boolean;
31
- websiteCount: number;
32
- }
33
-
34
- export function ActivityTimeline({
35
- processedEvents,
36
- isLoading,
37
- websiteCount,
38
- }: ActivityTimelineProps) {
39
- const [isTimelineCollapsed, setIsTimelineCollapsed] =
40
- useState<boolean>(false);
41
-
42
- const formatEventData = (data: any): string => {
43
- // Handle new structured data types
44
- if (typeof data === "object" && data !== null && data.type) {
45
- switch (data.type) {
46
- case 'functionCall':
47
- return `Calling function: ${data.name}\nArguments: ${JSON.stringify(data.args, null, 2)}`;
48
- case 'functionResponse':
49
- return `Function ${data.name} response:\n${JSON.stringify(data.response, null, 2)}`;
50
- case 'text':
51
- return data.content;
52
- case 'sources':
53
- const sources = data.content as Record<string, { title: string; url: string }>;
54
- if (Object.keys(sources).length === 0) {
55
- return "No sources found.";
56
- }
57
- return Object.values(sources)
58
- .map(source => `[${source.title || 'Untitled Source'}](${source.url})`).join(', ');
59
- default:
60
- return JSON.stringify(data, null, 2);
61
- }
62
- }
63
-
64
- // Existing logic for backward compatibility
65
- if (typeof data === "string") {
66
- // Try to parse as JSON first
67
- try {
68
- const parsed = JSON.parse(data);
69
- return JSON.stringify(parsed, null, 2);
70
- } catch {
71
- // If not JSON, return as string (could be markdown)
72
- return data;
73
- }
74
- } else if (Array.isArray(data)) {
75
- return data.join(", ");
76
- } else if (typeof data === "object" && data !== null) {
77
- return JSON.stringify(data, null, 2);
78
- }
79
- return String(data);
80
- };
81
-
82
- const isJsonData = (data: any): boolean => {
83
- // Handle new structured data types
84
- if (typeof data === "object" && data !== null && data.type) {
85
- if (data.type === 'sources') {
86
- return false; // Let ReactMarkdown handle this
87
- }
88
- return data.type === 'functionCall' || data.type === 'functionResponse';
89
- }
90
-
91
- // Existing logic
92
- if (typeof data === "string") {
93
- try {
94
- JSON.parse(data);
95
- return true;
96
- } catch {
97
- return false;
98
- }
99
- }
100
- return typeof data === "object" && data !== null;
101
- };
102
- const getEventIcon = (title: string, index: number) => {
103
- if (index === 0 && isLoading && processedEvents.length === 0) {
104
- return <Loader2 className="h-4 w-4 text-neutral-400 animate-spin" />;
105
- }
106
- if (title.toLowerCase().includes("function call")) {
107
- return <Activity className="h-4 w-4 text-blue-400" />;
108
- } else if (title.toLowerCase().includes("function response")) {
109
- return <Activity className="h-4 w-4 text-green-400" />;
110
- } else if (title.toLowerCase().includes("generating")) {
111
- return <TextSearch className="h-4 w-4 text-neutral-400" />;
112
- } else if (title.toLowerCase().includes("thinking")) {
113
- return <Loader2 className="h-4 w-4 text-neutral-400 animate-spin" />;
114
- } else if (title.toLowerCase().includes("reflection")) {
115
- return <Brain className="h-4 w-4 text-neutral-400" />;
116
- } else if (title.toLowerCase().includes("research")) {
117
- return <Search className="h-4 w-4 text-neutral-400" />;
118
- } else if (title.toLowerCase().includes("finalizing")) {
119
- return <Pen className="h-4 w-4 text-neutral-400" />;
120
- } else if (title.toLowerCase().includes("retrieved sources")) {
121
- return <Link className="h-4 w-4 text-yellow-400" />;
122
- }
123
- return <Activity className="h-4 w-4 text-neutral-400" />;
124
- };
125
-
126
- useEffect(() => {
127
- if (!isLoading && processedEvents.length !== 0) {
128
- setIsTimelineCollapsed(true);
129
- }
130
- }, [isLoading, processedEvents]);
131
- return (
132
- <Card className={`border-none rounded-lg bg-neutral-700 ${isTimelineCollapsed ? "h-10 py-2" : "max-h-96 py-2"}`}>
133
- <CardHeader className="py-0">
134
- <CardDescription className="flex items-center justify-between">
135
- <div
136
- className="flex items-center justify-start text-sm w-full cursor-pointer gap-2 text-neutral-100"
137
- onClick={() => setIsTimelineCollapsed(!isTimelineCollapsed)}
138
- >
139
- <span>Research</span>
140
- {websiteCount > 0 && (
141
- <span className="text-xs bg-neutral-600 px-2 py-0.5 rounded-full">
142
- {websiteCount} websites
143
- </span>
144
- )}
145
- {isTimelineCollapsed ? (
146
- <ChevronDown className="h-4 w-4 mr-2" />
147
- ) : (
148
- <ChevronUp className="h-4 w-4 mr-2" />
149
- )}
150
- </div>
151
- </CardDescription>
152
- </CardHeader>
153
- {!isTimelineCollapsed && (
154
- <ScrollArea className="max-h-80 overflow-y-auto">
155
- <CardContent>
156
- {isLoading && processedEvents.length === 0 && (
157
- <div className="relative pl-8 pb-4">
158
- <div className="absolute left-3 top-3.5 h-full w-0.5 bg-neutral-800" />
159
- <div className="absolute left-0.5 top-2 h-5 w-5 rounded-full bg-neutral-800 flex items-center justify-center ring-4 ring-neutral-900">
160
- <Loader2 className="h-3 w-3 text-neutral-400 animate-spin" />
161
- </div>
162
- <div>
163
- <p className="text-sm text-neutral-300 font-medium">
164
- Thinking...
165
- </p>
166
- </div>
167
- </div>
168
- )}
169
- {processedEvents.length > 0 ? (
170
- <div className="space-y-0">
171
- {processedEvents.map((eventItem, index) => (
172
- <div key={index} className="relative pl-8 pb-4">
173
- {index < processedEvents.length - 1 ||
174
- (isLoading && index === processedEvents.length - 1) ? (
175
- <div className="absolute left-3 top-3.5 h-full w-0.5 bg-neutral-600" />
176
- ) : null}
177
- <div className="absolute left-0.5 top-2 h-6 w-6 rounded-full bg-neutral-600 flex items-center justify-center ring-4 ring-neutral-700">
178
- {getEventIcon(eventItem.title, index)}
179
- </div>
180
- <div>
181
- <p className="text-sm text-neutral-200 font-medium mb-0.5">
182
- {eventItem.title}
183
- </p>
184
- <div className="text-xs text-neutral-300 leading-relaxed">
185
- {isJsonData(eventItem.data) ? (
186
- <pre className="bg-neutral-800 p-2 rounded text-xs overflow-x-auto whitespace-pre-wrap">
187
- {formatEventData(eventItem.data)}
188
- </pre>
189
- ) : (
190
- <ReactMarkdown
191
- components={{
192
- p: ({ children }) => <span>{children}</span>,
193
- a: ({ href, children }) => (
194
- <a
195
- href={href}
196
- target="_blank"
197
- rel="noopener noreferrer"
198
- className="text-blue-400 hover:text-blue-300 underline"
199
- >
200
- {children}
201
- </a>
202
- ),
203
- code: ({ children }) => (
204
- <code className="bg-neutral-800 px-1 py-0.5 rounded text-xs">
205
- {children}
206
- </code>
207
- ),
208
- }}
209
- >
210
- {formatEventData(eventItem.data)}
211
- </ReactMarkdown>
212
- )}
213
- </div>
214
- </div>
215
- </div>
216
- ))}
217
- {isLoading && processedEvents.length > 0 && (
218
- <div className="relative pl-8 pb-4">
219
- <div className="absolute left-0.5 top-2 h-5 w-5 rounded-full bg-neutral-600 flex items-center justify-center ring-4 ring-neutral-700">
220
- <Loader2 className="h-3 w-3 text-neutral-400 animate-spin" />
221
- </div>
222
- <div>
223
- <p className="text-sm text-neutral-300 font-medium">
224
- Thinking...
225
- </p>
226
- </div>
227
- </div>
228
- )}
229
- </div>
230
- ) : !isLoading ? ( // Only show "No activity" if not loading and no events
231
- <div className="flex flex-col items-center justify-center h-full text-neutral-500 pt-10">
232
- <Info className="h-6 w-6 mb-3" />
233
- <p className="text-sm">No activity to display.</p>
234
- <p className="text-xs text-neutral-600 mt-1">
235
- Timeline will update during processing.
236
- </p>
237
- </div>
238
- ) : null}
239
- </CardContent>
240
- </ScrollArea>
241
- )}
242
- </Card>
243
- );
244
- }
@@ -1,420 +0,0 @@
1
- import type React from "react";
2
- import { ScrollArea } from "@/components/ui/scroll-area";
3
- import { Loader2, Copy, CopyCheck } from "lucide-react";
4
- import { InputForm } from "@/components/InputForm";
5
- import { Button } from "@/components/ui/button";
6
- import { useState, ReactNode } from "react";
7
- import ReactMarkdown from "react-markdown";
8
- import remarkGfm from 'remark-gfm';
9
- import { cn } from "@/utils";
10
- import { Badge } from "@/components/ui/badge";
11
- import { ActivityTimeline } from "@/components/ActivityTimeline";
12
-
13
- // Markdown component props type from former ReportView
14
- type MdComponentProps = {
15
- className?: string;
16
- children?: ReactNode;
17
- [key: string]: any;
18
- };
19
-
20
- interface ProcessedEvent {
21
- title: string;
22
- data: any;
23
- }
24
-
25
- // Markdown components (from former ReportView.tsx)
26
- const mdComponents = {
27
- h1: ({ className, children, ...props }: MdComponentProps) => (
28
- <h1 className={cn("text-2xl font-bold mt-4 mb-2", className)} {...props}>
29
- {children}
30
- </h1>
31
- ),
32
- h2: ({ className, children, ...props }: MdComponentProps) => (
33
- <h2 className={cn("text-xl font-bold mt-3 mb-2", className)} {...props}>
34
- {children}
35
- </h2>
36
- ),
37
- h3: ({ className, children, ...props }: MdComponentProps) => (
38
- <h3 className={cn("text-lg font-bold mt-3 mb-1", className)} {...props}>
39
- {children}
40
- </h3>
41
- ),
42
- p: ({ className, children, ...props }: MdComponentProps) => (
43
- <p className={cn("mb-3 leading-7", className)} {...props}>
44
- {children}
45
- </p>
46
- ),
47
- a: ({ className, children, href, ...props }: MdComponentProps) => (
48
- <Badge className="text-xs mx-0.5">
49
- <a
50
- className={cn("text-blue-400 hover:text-blue-300 text-xs", className)}
51
- href={href}
52
- target="_blank"
53
- rel="noopener noreferrer"
54
- {...props}
55
- >
56
- {children}
57
- </a>
58
- </Badge>
59
- ),
60
- ul: ({ className, children, ...props }: MdComponentProps) => (
61
- <ul className={cn("list-disc pl-6 mb-3", className)} {...props}>
62
- {children}
63
- </ul>
64
- ),
65
- ol: ({ className, children, ...props }: MdComponentProps) => (
66
- <ol className={cn("list-decimal pl-6 mb-3", className)} {...props}>
67
- {children}
68
- </ol>
69
- ),
70
- li: ({ className, children, ...props }: MdComponentProps) => (
71
- <li className={cn("mb-1", className)} {...props}>
72
- {children}
73
- </li>
74
- ),
75
- blockquote: ({ className, children, ...props }: MdComponentProps) => (
76
- <blockquote
77
- className={cn(
78
- "border-l-4 border-neutral-600 pl-4 italic my-3 text-sm",
79
- className
80
- )}
81
- {...props}
82
- >
83
- {children}
84
- </blockquote>
85
- ),
86
- code: ({ className, children, ...props }: MdComponentProps) => (
87
- <code
88
- className={cn(
89
- "bg-neutral-900 rounded px-1 py-0.5 font-mono text-xs",
90
- className
91
- )}
92
- {...props}
93
- >
94
- {children}
95
- </code>
96
- ),
97
- pre: ({ className, children, ...props }: MdComponentProps) => (
98
- <pre
99
- className={cn(
100
- "bg-neutral-900 p-3 rounded-lg overflow-x-auto font-mono text-xs my-3",
101
- className
102
- )}
103
- {...props}
104
- >
105
- {children}
106
- </pre>
107
- ),
108
- hr: ({ className, ...props }: MdComponentProps) => (
109
- <hr className={cn("border-neutral-600 my-4", className)} {...props} />
110
- ),
111
- table: ({ className, children, ...props }: MdComponentProps) => (
112
- <div className="my-3 overflow-x-auto">
113
- <table className={cn("border-collapse w-full", className)} {...props}>
114
- {children}
115
- </table>
116
- </div>
117
- ),
118
- th: ({ className, children, ...props }: MdComponentProps) => (
119
- <th
120
- className={cn(
121
- "border border-neutral-600 px-3 py-2 text-left font-bold",
122
- className
123
- )}
124
- {...props}
125
- >
126
- {children}
127
- </th>
128
- ),
129
- td: ({ className, children, ...props }: MdComponentProps) => (
130
- <td
131
- className={cn("border border-neutral-600 px-3 py-2", className)}
132
- {...props}
133
- >
134
- {children}
135
- </td>
136
- ),
137
- };
138
-
139
- // Props for HumanMessageBubble
140
- interface HumanMessageBubbleProps {
141
- message: { content: string; id: string };
142
- mdComponents: typeof mdComponents;
143
- }
144
-
145
- // HumanMessageBubble Component
146
- const HumanMessageBubble: React.FC<HumanMessageBubbleProps> = ({
147
- message,
148
- mdComponents,
149
- }) => {
150
- return (
151
- <div className="text-white rounded-3xl break-words min-h-7 bg-neutral-700 max-w-[100%] sm:max-w-[90%] px-4 pt-3 rounded-br-lg">
152
- <ReactMarkdown components={mdComponents} remarkPlugins={[remarkGfm]}>
153
- {message.content}
154
- </ReactMarkdown>
155
- </div>
156
- );
157
- };
158
-
159
- // Props for AiMessageBubble
160
- interface AiMessageBubbleProps {
161
- message: { content: string; id: string };
162
- mdComponents: typeof mdComponents;
163
- handleCopy: (text: string, messageId: string) => void;
164
- copiedMessageId: string | null;
165
- agent?: string;
166
- finalReportWithCitations?: boolean;
167
- processedEvents: ProcessedEvent[];
168
- websiteCount: number;
169
- isLoading: boolean;
170
- }
171
-
172
- // AiMessageBubble Component
173
- const AiMessageBubble: React.FC<AiMessageBubbleProps> = ({
174
- message,
175
- mdComponents,
176
- handleCopy,
177
- copiedMessageId,
178
- agent,
179
- finalReportWithCitations,
180
- processedEvents,
181
- websiteCount,
182
- isLoading,
183
- }) => {
184
- // Show ActivityTimeline if we have processedEvents (this will be the first AI message)
185
- const shouldShowTimeline = processedEvents.length > 0;
186
-
187
- // Condition for DIRECT DISPLAY (interactive_planner_agent OR final report)
188
- const shouldDisplayDirectly =
189
- agent === "interactive_planner_agent" ||
190
- (agent === "report_composer_with_citations" && finalReportWithCitations);
191
-
192
- if (shouldDisplayDirectly) {
193
- // Direct display - show content with copy button, and timeline if available
194
- return (
195
- <div className="relative break-words flex flex-col w-full">
196
- {/* Show timeline for interactive_planner_agent if available */}
197
- {shouldShowTimeline && agent === "interactive_planner_agent" && (
198
- <div className="w-full mb-2">
199
- <ActivityTimeline
200
- processedEvents={processedEvents}
201
- isLoading={isLoading}
202
- websiteCount={websiteCount}
203
- />
204
- </div>
205
- )}
206
- <div className="flex items-start gap-3">
207
- <div className="flex-1">
208
- <ReactMarkdown components={mdComponents} remarkPlugins={[remarkGfm]}>
209
- {message.content}
210
- </ReactMarkdown>
211
- </div>
212
- <button
213
- onClick={() => handleCopy(message.content, message.id)}
214
- className="p-1 hover:bg-neutral-700 rounded"
215
- >
216
- {copiedMessageId === message.id ? (
217
- <CopyCheck className="h-4 w-4 text-green-500" />
218
- ) : (
219
- <Copy className="h-4 w-4 text-neutral-400" />
220
- )}
221
- </button>
222
- </div>
223
- </div>
224
- );
225
- } else if (shouldShowTimeline) {
226
- // First AI message with timeline only (no direct content display)
227
- return (
228
- <div className="relative break-words flex flex-col w-full">
229
- <div className="w-full">
230
- <ActivityTimeline
231
- processedEvents={processedEvents}
232
- isLoading={isLoading}
233
- websiteCount={websiteCount}
234
- />
235
- </div>
236
- {/* Only show accumulated content if it's not empty and not from research agents */}
237
- {message.content && message.content.trim() && agent !== "interactive_planner_agent" && (
238
- <div className="flex items-start gap-3 mt-2">
239
- <div className="flex-1">
240
- <ReactMarkdown components={mdComponents} remarkPlugins={[remarkGfm]}>
241
- {message.content}
242
- </ReactMarkdown>
243
- </div>
244
- <button
245
- onClick={() => handleCopy(message.content, message.id)}
246
- className="p-1 hover:bg-neutral-700 rounded"
247
- >
248
- {copiedMessageId === message.id ? (
249
- <CopyCheck className="h-4 w-4 text-green-500" />
250
- ) : (
251
- <Copy className="h-4 w-4 text-neutral-400" />
252
- )}
253
- </button>
254
- </div>
255
- )}
256
- </div>
257
- );
258
- } else {
259
- // Fallback for other messages - just show content
260
- return (
261
- <div className="relative break-words flex flex-col w-full">
262
- <div className="flex items-start gap-3">
263
- <div className="flex-1">
264
- <ReactMarkdown components={mdComponents} remarkPlugins={[remarkGfm]}>
265
- {message.content}
266
- </ReactMarkdown>
267
- </div>
268
- <button
269
- onClick={() => handleCopy(message.content, message.id)}
270
- className="p-1 hover:bg-neutral-700 rounded"
271
- >
272
- {copiedMessageId === message.id ? (
273
- <CopyCheck className="h-4 w-4 text-green-500" />
274
- ) : (
275
- <Copy className="h-4 w-4 text-neutral-400" />
276
- )}
277
- </button>
278
- </div>
279
- </div>
280
- );
281
- }
282
- };
283
-
284
- interface ChatMessagesViewProps {
285
- messages: { type: "human" | "ai"; content: string; id: string; agent?: string; finalReportWithCitations?: boolean }[];
286
- isLoading: boolean;
287
- scrollAreaRef: React.RefObject<HTMLDivElement | null>;
288
- onSubmit: (query: string) => void;
289
- onCancel: () => void;
290
- displayData: string | null;
291
- messageEvents: Map<string, ProcessedEvent[]>;
292
- websiteCount: number;
293
- }
294
-
295
- export function ChatMessagesView({
296
- messages,
297
- isLoading,
298
- scrollAreaRef,
299
- onSubmit,
300
- onCancel,
301
- messageEvents,
302
- websiteCount,
303
- }: ChatMessagesViewProps) {
304
- const [copiedMessageId, setCopiedMessageId] = useState<string | null>(null);
305
-
306
- const handleCopy = async (text: string, messageId: string) => {
307
- try {
308
- await navigator.clipboard.writeText(text);
309
- setCopiedMessageId(messageId);
310
- setTimeout(() => setCopiedMessageId(null), 2000);
311
- } catch (err) {
312
- console.error("Failed to copy text:", err);
313
- }
314
- };
315
-
316
- const handleNewChat = () => {
317
- window.location.reload();
318
- };
319
-
320
- // Find the ID of the last AI message
321
- const lastAiMessage = messages.slice().reverse().find(m => m.type === "ai");
322
- const lastAiMessageId = lastAiMessage?.id;
323
-
324
- return (
325
- <div className="flex flex-col h-full w-full">
326
- {/* Header with New Chat button */}
327
- <div className="border-b border-neutral-700 p-4 bg-neutral-800">
328
- <div className="max-w-4xl mx-auto flex justify-between items-center">
329
- <h1 className="text-lg font-semibold text-neutral-100">Chat</h1>
330
- <Button
331
- onClick={handleNewChat}
332
- variant="outline"
333
- className="bg-neutral-700 hover:bg-neutral-600 text-neutral-100 border-neutral-600 hover:border-neutral-500"
334
- >
335
- New Chat
336
- </Button>
337
- </div>
338
- </div>
339
- <div className="flex-1 flex flex-col w-full">
340
- <ScrollArea ref={scrollAreaRef} className="flex-1 w-full">
341
- <div className="p-4 md:p-6 space-y-2 max-w-4xl mx-auto">
342
- {messages.map((message) => { // Removed index as it's not directly used for this logic
343
- const eventsForMessage = message.type === "ai" ? (messageEvents.get(message.id) || []) : [];
344
-
345
- // Determine if the current AI message is the last one
346
- const isCurrentMessageTheLastAiMessage = message.type === "ai" && message.id === lastAiMessageId;
347
-
348
- return (
349
- <div
350
- key={message.id}
351
- className={`flex ${message.type === "human" ? "justify-end" : "justify-start"}`}
352
- >
353
- {message.type === "human" ? (
354
- <HumanMessageBubble
355
- message={message}
356
- mdComponents={mdComponents}
357
- />
358
- ) : (
359
- <AiMessageBubble
360
- message={message}
361
- mdComponents={mdComponents}
362
- handleCopy={handleCopy}
363
- copiedMessageId={copiedMessageId}
364
- agent={message.agent}
365
- finalReportWithCitations={message.finalReportWithCitations}
366
- processedEvents={eventsForMessage}
367
- // MODIFIED: Pass websiteCount only if it's the last AI message
368
- websiteCount={isCurrentMessageTheLastAiMessage ? websiteCount : 0}
369
- // MODIFIED: Pass isLoading only if it's the last AI message and global isLoading is true
370
- isLoading={isCurrentMessageTheLastAiMessage && isLoading}
371
- />
372
- )}
373
- </div>
374
- );
375
- })}
376
- {/* This global "Thinking..." indicator appears below all messages if isLoading is true */}
377
- {/* It's independent of the per-timeline isLoading state */}
378
- {isLoading && !lastAiMessage && messages.some(m => m.type === 'human') && (
379
- <div className="flex justify-start">
380
- <div className="flex items-center gap-2 text-neutral-400">
381
- <Loader2 className="h-4 w-4 animate-spin" />
382
- <span>Thinking...</span>
383
- </div>
384
- </div>
385
- )}
386
- {/* Show "Thinking..." if the last message is human and we are loading,
387
- or if there's an active AI message that is the last one and we are loading.
388
- The AiMessageBubble's internal isLoading will handle its own spinner.
389
- This one is for the general loading state at the bottom.
390
- */}
391
- {isLoading && messages.length > 0 && messages[messages.length -1].type === 'human' && (
392
- <div className="flex justify-start pl-10 pt-2"> {/* Adjusted padding to align similarly to AI bubble */}
393
- <div className="flex items-center gap-2 text-neutral-400">
394
- <Loader2 className="h-4 w-4 animate-spin" />
395
- <span>Thinking...</span>
396
- </div>
397
- </div>
398
- )}
399
- </div>
400
- </ScrollArea>
401
- </div>
402
- <div className="border-t border-neutral-700 p-4 w-full">
403
- <div className="max-w-3xl mx-auto">
404
- <InputForm onSubmit={onSubmit} isLoading={isLoading} context="chat" />
405
- {isLoading && (
406
- <div className="mt-4 flex justify-center">
407
- <Button
408
- variant="outline"
409
- onClick={onCancel}
410
- className="text-red-400 hover:text-red-300"
411
- >
412
- Cancel
413
- </Button>
414
- </div>
415
- )}
416
- </div>
417
- </div>
418
- </div>
419
- );
420
- }