Skip to content

Commit ba2b6e2

Browse files
authored
Add document viewer and prompt edit feature (#11)
- Adds ability to open a full screen view of the markdown documents. - Adds ability to edit the planning agent system prompt.
1 parent 19edd8b commit ba2b6e2

File tree

8 files changed

+1510
-91
lines changed

8 files changed

+1510
-91
lines changed

config/gni/devtools_grd_files.gni

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -602,11 +602,13 @@ grd_files_release_sources = [
602602
"front_end/panels/ai_chat/ui/ChatView.js",
603603
"front_end/panels/ai_chat/ui/chatView.css.js",
604604
"front_end/panels/ai_chat/ui/HelpDialog.js",
605+
"front_end/panels/ai_chat/ui/PromptEditDialog.js",
605606
"front_end/panels/ai_chat/ui/SettingsDialog.js",
606607
"front_end/panels/ai_chat/core/AgentService.js",
607608
"front_end/panels/ai_chat/core/State.js",
608609
"front_end/panels/ai_chat/core/Graph.js",
609610
"front_end/panels/ai_chat/core/Types.js",
611+
"front_end/panels/ai_chat/core/Constants.js",
610612
"front_end/panels/ai_chat/core/ConfigurableGraph.js",
611613
"front_end/panels/ai_chat/core/GraphConfigs.js",
612614
"front_end/panels/ai_chat/core/OpenAIClient.js",

front_end/panels/ai_chat/BUILD.gn

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ devtools_module("ai_chat") {
2020
"ui/ChatView.ts",
2121
"ui/HelpDialog.ts",
2222
"ui/SettingsDialog.ts",
23+
"ui/PromptEditDialog.ts",
2324
"ai_chat_impl.ts",
2425
"core/Graph.ts",
2526
"core/State.ts",
@@ -76,6 +77,7 @@ _ai_chat_sources = [
7677
"ui/AIChatPanel.ts",
7778
"ui/ChatView.ts",
7879
"ui/HelpDialog.ts",
80+
"ui/PromptEditDialog.ts",
7981
"ui/SettingsDialog.ts",
8082
"ai_chat_impl.ts",
8183
"core/Graph.ts",

front_end/panels/ai_chat/core/BaseOrchestratorAgent.ts

Lines changed: 178 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,13 @@
33
// found in the LICENSE file.
44

55
import * as Lit from '../../../ui/lit/lit.js';
6+
const {html} = Lit;
7+
8+
// Constants
9+
const PROMPT_CONSTANTS = {
10+
DOUBLE_CLICK_DELAY: 300,
11+
CUSTOM_PROMPTS_STORAGE_KEY: 'ai_chat_custom_prompts',
12+
} as const;
613

714
// Direct imports from Tools.ts
815
import { ToolRegistry } from '../agent_framework/ConfigurableAgentTool.js';
@@ -23,8 +30,6 @@ import {
2330
// Initialize configured agents
2431
initializeConfiguredAgents();
2532

26-
const {html} = Lit;
27-
2833
// Define available agent types
2934
export enum BaseOrchestratorAgentType {
3035
SEARCH = 'search',
@@ -67,7 +72,21 @@ Present your findings in a structured markdown report with:
6772
7. **Conclusions**: Summary of the most reliable answers based on the research
6873
8. **References**: Full citation list of all sources consulted
6974
70-
Maintain objectivity throughout your research process and clearly distinguish between well-established facts and more speculative information. When appropriate, note areas where more research might be needed. Note: the final report should be alteast 5000 words or even longer based on the topic, if there is not enough content do more research.`,
75+
Maintain objectivity throughout your research process and clearly distinguish between well-established facts and more speculative information. When appropriate, note areas where more research might be needed. Note: the final report should be at least 5000 words or even longer based on the topic, if there is not enough content do more research.
76+
77+
## CRITICAL: Final Output Format
78+
79+
When calling 'finalize_with_critique', you MUST structure your response in this exact XML format:
80+
81+
<reasoning>
82+
[Provide 2-3 sentences explaining your research approach, key insights discovered, and how you organized the information]
83+
</reasoning>
84+
85+
<markdown_report>
86+
[Your comprehensive markdown report goes here - this will be automatically extracted and displayed in an enhanced document viewer]
87+
</markdown_report>
88+
89+
The markdown report section will be hidden from the chat interface and displayed with an enhanced document viewer button. Only the reasoning will be shown in the chat.`,
7190

7291
[BaseOrchestratorAgentType.SHOPPING]: `You are a **Shopping Research Agent**. Your mission is to help users find and compare products tailored to their specific needs and budget, providing up-to-date, unbiased, and well-cited recommendations.
7392
@@ -217,6 +236,11 @@ export const AGENT_CONFIGS: {[key: string]: AgentConfig} = {
217236
* Get the system prompt for a specific agent type
218237
*/
219238
export function getSystemPrompt(agentType: string): string {
239+
// Check if there's a custom prompt for this agent type
240+
if (hasCustomPrompt(agentType)) {
241+
return getAgentPrompt(agentType);
242+
}
243+
220244
return AGENT_CONFIGS[agentType]?.systemPrompt ||
221245
// Default system prompt if agent type not found
222246
`
@@ -295,17 +319,30 @@ export function renderAgentTypeButtons(
295319
): Lit.TemplateResult {
296320
return html`
297321
<div class="prompt-buttons-container">
298-
${Object.values(AGENT_CONFIGS).map(config => html`
322+
${Object.values(AGENT_CONFIGS).map(config => {
323+
const isCustomized = hasCustomPrompt(config.type);
324+
const buttonClasses = [
325+
'prompt-button',
326+
selectedAgentType === config.type ? 'selected' : '',
327+
isCustomized ? 'customized' : ''
328+
].filter(Boolean).join(' ');
329+
330+
const title = isCustomized ?
331+
`${config.description || config.label} (Custom prompt - double-click to edit)` :
332+
`${config.description || config.label} (Double-click to edit prompt)`;
333+
334+
return html`
299335
<button
300-
class="prompt-button ${selectedAgentType === config.type ? 'selected' : ''}"
336+
class=${buttonClasses}
301337
data-agent-type=${config.type}
302338
@click=${handleClick}
303-
title=${config.description || config.label}
339+
title=${title}
304340
>
305341
<span class="prompt-icon">${config.icon}</span>
306342
${showLabels ? html`<span class="prompt-label">${config.label}</span>` : Lit.nothing}
343+
${isCustomized ? html`<span class="prompt-custom-indicator"></span>` : Lit.nothing}
307344
</button>
308-
`)}
345+
`})}
309346
</div>
310347
`;
311348
}
@@ -316,38 +353,151 @@ export function createAgentTypeSelectionHandler(
316353
textInputElement: HTMLTextAreaElement | undefined,
317354
onAgentTypeSelected: ((agentType: string | null) => void) | undefined,
318355
setSelectedAgentType: (type: string | null) => void,
319-
getCurrentSelectedType: () => string | null
356+
getCurrentSelectedType: () => string | null,
357+
onAgentPromptEdit?: (agentType: string) => void
320358
): (event: Event) => void {
359+
let clickTimeout: number | null = null;
360+
let clickCount = 0;
361+
321362
return (event: Event): void => {
322363
const button = event.currentTarget as HTMLButtonElement;
323364
const agentType = button.dataset.agentType;
324365
if (agentType && onAgentTypeSelected) {
325-
const currentSelected = getCurrentSelectedType();
366+
clickCount++;
326367

327-
// Remove selected class from all agent type buttons
328-
const allButtons = element.shadowRoot?.querySelectorAll('.prompt-button');
329-
allButtons?.forEach(btn => btn.classList.remove('selected'));
330-
331-
// Check if we're clicking on the currently selected button (toggle off)
332-
if (currentSelected === agentType) {
333-
// Deselect - set to null and don't add selected class
334-
setSelectedAgentType(null);
335-
onAgentTypeSelected(null);
336-
console.log('Deselected agent type, returning to default');
337-
} else {
338-
// Select new agent type - add selected class to clicked button
339-
button.classList.add('selected');
340-
setSelectedAgentType(agentType);
341-
onAgentTypeSelected(agentType);
342-
console.log('Selected agent type:', agentType);
368+
// Clear existing timeout
369+
if (clickTimeout) {
370+
clearTimeout(clickTimeout);
343371
}
344-
345-
// Focus the input after selecting/deselecting an agent type
346-
textInputElement?.focus();
372+
373+
// Set timeout to distinguish between single and double click
374+
clickTimeout = window.setTimeout(() => {
375+
if (clickCount === 1) {
376+
// Single click - handle selection/deselection
377+
const currentSelected = getCurrentSelectedType();
378+
379+
// Remove selected class from all agent type buttons
380+
const allButtons = element.shadowRoot?.querySelectorAll('.prompt-button');
381+
allButtons?.forEach(btn => btn.classList.remove('selected'));
382+
383+
// Check if we're clicking on the currently selected button (toggle off)
384+
if (currentSelected === agentType) {
385+
// Deselect - set to null and don't add selected class
386+
setSelectedAgentType(null);
387+
onAgentTypeSelected(null);
388+
console.log('Deselected agent type, returning to default');
389+
} else {
390+
// Select new agent type - add selected class to clicked button
391+
button.classList.add('selected');
392+
setSelectedAgentType(agentType);
393+
onAgentTypeSelected(agentType);
394+
console.log('Selected agent type:', agentType);
395+
}
396+
397+
// Focus the input after selecting/deselecting an agent type
398+
textInputElement?.focus();
399+
} else if (clickCount === 2 && onAgentPromptEdit) {
400+
// Double click - handle prompt editing
401+
console.log('Double-clicked agent type for prompt editing:', agentType);
402+
onAgentPromptEdit(agentType);
403+
}
404+
405+
clickCount = 0;
406+
clickTimeout = null;
407+
}, PROMPT_CONSTANTS.DOUBLE_CLICK_DELAY);
347408
}
348409
};
349410
}
350411

412+
// Prompt management functions
413+
414+
/**
415+
* Get the current prompt for an agent type (custom or default)
416+
*/
417+
export function getAgentPrompt(agentType: string): string {
418+
const customPrompts = getCustomPrompts();
419+
return customPrompts[agentType] || SYSTEM_PROMPTS[agentType as keyof typeof SYSTEM_PROMPTS] || '';
420+
}
421+
422+
/**
423+
* Set a custom prompt for an agent type
424+
*/
425+
export function setCustomPrompt(agentType: string, prompt: string): void {
426+
try {
427+
const customPrompts = getCustomPrompts();
428+
customPrompts[agentType] = prompt;
429+
localStorage.setItem(PROMPT_CONSTANTS.CUSTOM_PROMPTS_STORAGE_KEY, JSON.stringify(customPrompts));
430+
} catch (error) {
431+
console.error('Failed to save custom prompt:', error);
432+
throw error;
433+
}
434+
}
435+
436+
/**
437+
* Remove custom prompt for an agent type (restore to default)
438+
*/
439+
export function removeCustomPrompt(agentType: string): void {
440+
try {
441+
const customPrompts = getCustomPrompts();
442+
delete customPrompts[agentType];
443+
localStorage.setItem(PROMPT_CONSTANTS.CUSTOM_PROMPTS_STORAGE_KEY, JSON.stringify(customPrompts));
444+
} catch (error) {
445+
console.error('Failed to remove custom prompt:', error);
446+
throw error;
447+
}
448+
}
449+
450+
/**
451+
* Check if an agent type has a custom prompt
452+
*/
453+
export function hasCustomPrompt(agentType: string): boolean {
454+
const customPrompts = getCustomPrompts();
455+
return agentType in customPrompts;
456+
}
457+
458+
/**
459+
* Get all custom prompts from localStorage
460+
*/
461+
function getCustomPrompts(): {[key: string]: string} {
462+
try {
463+
const stored = localStorage.getItem(PROMPT_CONSTANTS.CUSTOM_PROMPTS_STORAGE_KEY);
464+
if (!stored) {
465+
return {};
466+
}
467+
const parsed = JSON.parse(stored);
468+
// Validate that it's an object with string values
469+
if (typeof parsed !== 'object' || parsed === null) {
470+
console.warn('Invalid custom prompts format, resetting');
471+
return {};
472+
}
473+
// Ensure all values are strings
474+
const validated: {[key: string]: string} = {};
475+
for (const [key, value] of Object.entries(parsed)) {
476+
if (typeof value === 'string') {
477+
validated[key] = value;
478+
}
479+
}
480+
return validated;
481+
} catch (error) {
482+
console.error('Error loading custom prompts:', error);
483+
return {};
484+
}
485+
}
486+
487+
/**
488+
* Get the default prompt for an agent type
489+
*/
490+
export function getDefaultPrompt(agentType: string): string {
491+
return SYSTEM_PROMPTS[agentType as keyof typeof SYSTEM_PROMPTS] || '';
492+
}
493+
494+
/**
495+
* Type guard to check if an agent type is valid
496+
*/
497+
export function isValidAgentType(agentType: string): agentType is BaseOrchestratorAgentType {
498+
return Object.values(BaseOrchestratorAgentType).includes(agentType as BaseOrchestratorAgentType);
499+
}
500+
351501
declare global {
352502
interface HTMLElementEventMap {
353503
[AgentTypeSelectionEvent.eventName]: AgentTypeSelectionEvent;
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// Copyright 2025 The Chromium Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
/**
6+
* Constants used throughout the AI Chat panel
7+
*/
8+
9+
// Timing constants (in milliseconds)
10+
export const TIMING_CONSTANTS = {
11+
// UI feedback durations
12+
COPY_FEEDBACK_DURATION: 2000,
13+
STATUS_MESSAGE_DURATION: 3000,
14+
DOUBLE_CLICK_DELAY: 300,
15+
16+
// AI Assistant loading
17+
AI_ASSISTANT_LOAD_TIMEOUT: 3000,
18+
AI_ASSISTANT_RETRY_DELAY: 1000,
19+
AI_ASSISTANT_MAX_RETRIES: 3,
20+
} as const;
21+
22+
// Content detection thresholds
23+
export const CONTENT_THRESHOLDS = {
24+
// Deep research detection
25+
DEEP_RESEARCH_MIN_LENGTH: 500,
26+
DEEP_RESEARCH_MIN_HEADINGS: 4,
27+
28+
// Document viewer
29+
MARKDOWN_REPORT_MIN_LENGTH: 100,
30+
} as const;
31+
32+
// Storage keys
33+
export const STORAGE_KEYS = {
34+
CUSTOM_PROMPTS: 'ai_chat_custom_prompts',
35+
LITELLM_MODELS: 'litellm_custom_models',
36+
API_KEY: 'ai_chat_api_key',
37+
LITELLM_ENDPOINT: 'litellm_endpoint',
38+
SELECTED_MODEL: 'selected_model',
39+
} as const;
40+
41+
// Dialog dimensions
42+
export const DIALOG_DIMENSIONS = {
43+
PROMPT_EDIT_MIN_WIDTH: 600,
44+
PROMPT_EDIT_MIN_HEIGHT: 500,
45+
PROMPT_EDIT_TEXTAREA_ROWS: 20,
46+
PROMPT_EDIT_TEXTAREA_COLS: 80,
47+
} as const;
48+
49+
// Default values
50+
export const DEFAULTS = {
51+
MAX_TOKENS: 4096,
52+
TEMPERATURE: 0.7,
53+
MODEL: 'gpt-4o',
54+
} as const;
55+
56+
// Regular expressions
57+
export const REGEX_PATTERNS = {
58+
// XML parsing patterns
59+
REASONING_TAG: /<reasoning>\s*([\s\S]*?)\s*<\/reasoning>/,
60+
MARKDOWN_REPORT_TAG: /<markdown_report>\s*([\s\S]*?)\s*<\/markdown_report>/,
61+
62+
// Markdown patterns
63+
HEADING: /^#{1,6}\s+.+$/gm,
64+
LIST_ITEM: /^[\*\-]\s+.+$/gm,
65+
NUMBERED_LIST: /^\d+\.\s+.+$/gm,
66+
} as const;
67+
68+
// Error messages
69+
export const ERROR_MESSAGES = {
70+
NO_API_KEY: 'Please configure your API key in settings',
71+
INVALID_API_KEY: 'Invalid API key. Please check your settings',
72+
STORAGE_QUOTA_EXCEEDED: 'Storage quota exceeded. Please clear some space and try again.',
73+
AI_ASSISTANT_LOAD_FAILED: 'Failed to load AI Assistant. Please try again.',
74+
NO_PRIMARY_TARGET: 'No primary page target found',
75+
EMPTY_PROMPT: 'Prompt cannot be empty',
76+
} as const;

0 commit comments

Comments
 (0)