Support web search (#1370)
<!-- This is an auto-generated description by cubic. --> ## Summary by cubic Adds web search to Dyad Pro chats with a new UI, tag parsing, and a Pro Mode toggle that wires through to the engine. - **New Features** - Pro Mode toggle: “Web Search” (settings.enableProWebSearch). - New custom tags: dyad-web-search, dyad-web-search-result, dyad-read. - Collapsible Web Search Result UI with in-progress badge and markdown rendering. - Engine integration: passes enable_web_search and activates DyadEngine when web search is on. <!-- End of auto-generated description by cubic. -->
This commit is contained in:
@@ -19,6 +19,12 @@ import { hasDyadProKey, type UserSettings } from "@/lib/schemas";
|
|||||||
export function ProModeSelector() {
|
export function ProModeSelector() {
|
||||||
const { settings, updateSettings } = useSettings();
|
const { settings, updateSettings } = useSettings();
|
||||||
|
|
||||||
|
const toggleWebSearch = () => {
|
||||||
|
updateSettings({
|
||||||
|
enableProWebSearch: !settings?.enableProWebSearch,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const toggleLazyEdits = () => {
|
const toggleLazyEdits = () => {
|
||||||
updateSettings({
|
updateSettings({
|
||||||
enableProLazyEditsMode: !settings?.enableProLazyEditsMode,
|
enableProLazyEditsMode: !settings?.enableProLazyEditsMode,
|
||||||
@@ -105,6 +111,15 @@ export function ProModeSelector() {
|
|||||||
settingEnabled={Boolean(settings?.enableDyadPro)}
|
settingEnabled={Boolean(settings?.enableDyadPro)}
|
||||||
toggle={toggleProEnabled}
|
toggle={toggleProEnabled}
|
||||||
/>
|
/>
|
||||||
|
<SelectorRow
|
||||||
|
id="web-search"
|
||||||
|
label="Web Search"
|
||||||
|
description="Search the web for information"
|
||||||
|
tooltip="Uses the web to search for information"
|
||||||
|
isTogglable={proModeTogglable}
|
||||||
|
settingEnabled={Boolean(settings?.enableProWebSearch)}
|
||||||
|
toggle={toggleWebSearch}
|
||||||
|
/>
|
||||||
<SelectorRow
|
<SelectorRow
|
||||||
id="lazy-edits"
|
id="lazy-edits"
|
||||||
label="Turbo Edits"
|
label="Turbo Edits"
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ import { DyadProblemSummary } from "./DyadProblemSummary";
|
|||||||
import { IpcClient } from "@/ipc/ipc_client";
|
import { IpcClient } from "@/ipc/ipc_client";
|
||||||
import { DyadMcpToolCall } from "./DyadMcpToolCall";
|
import { DyadMcpToolCall } from "./DyadMcpToolCall";
|
||||||
import { DyadMcpToolResult } from "./DyadMcpToolResult";
|
import { DyadMcpToolResult } from "./DyadMcpToolResult";
|
||||||
|
import { DyadWebSearchResult } from "./DyadWebSearchResult";
|
||||||
|
import { DyadWebSearch } from "./DyadWebSearch";
|
||||||
|
import { DyadRead } from "./DyadRead";
|
||||||
|
|
||||||
interface DyadMarkdownParserProps {
|
interface DyadMarkdownParserProps {
|
||||||
content: string;
|
content: string;
|
||||||
@@ -124,6 +127,9 @@ function preprocessUnclosedTags(content: string): {
|
|||||||
"dyad-chat-summary",
|
"dyad-chat-summary",
|
||||||
"dyad-edit",
|
"dyad-edit",
|
||||||
"dyad-codebase-context",
|
"dyad-codebase-context",
|
||||||
|
"dyad-web-search-result",
|
||||||
|
"dyad-web-search",
|
||||||
|
"dyad-read",
|
||||||
"think",
|
"think",
|
||||||
"dyad-command",
|
"dyad-command",
|
||||||
"dyad-mcp-tool-call",
|
"dyad-mcp-tool-call",
|
||||||
@@ -193,6 +199,9 @@ function parseCustomTags(content: string): ContentPiece[] {
|
|||||||
"dyad-chat-summary",
|
"dyad-chat-summary",
|
||||||
"dyad-edit",
|
"dyad-edit",
|
||||||
"dyad-codebase-context",
|
"dyad-codebase-context",
|
||||||
|
"dyad-web-search-result",
|
||||||
|
"dyad-web-search",
|
||||||
|
"dyad-read",
|
||||||
"think",
|
"think",
|
||||||
"dyad-command",
|
"dyad-command",
|
||||||
"dyad-mcp-tool-call",
|
"dyad-mcp-tool-call",
|
||||||
@@ -282,6 +291,40 @@ function renderCustomTag(
|
|||||||
const { tag, attributes, content, inProgress } = tagInfo;
|
const { tag, attributes, content, inProgress } = tagInfo;
|
||||||
|
|
||||||
switch (tag) {
|
switch (tag) {
|
||||||
|
case "dyad-read":
|
||||||
|
return (
|
||||||
|
<DyadRead
|
||||||
|
node={{
|
||||||
|
properties: {
|
||||||
|
path: attributes.path || "",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</DyadRead>
|
||||||
|
);
|
||||||
|
case "dyad-web-search":
|
||||||
|
return (
|
||||||
|
<DyadWebSearch
|
||||||
|
node={{
|
||||||
|
properties: {},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</DyadWebSearch>
|
||||||
|
);
|
||||||
|
case "dyad-web-search-result":
|
||||||
|
return (
|
||||||
|
<DyadWebSearchResult
|
||||||
|
node={{
|
||||||
|
properties: {
|
||||||
|
state: getState({ isStreaming, inProgress }),
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</DyadWebSearchResult>
|
||||||
|
);
|
||||||
case "think":
|
case "think":
|
||||||
return (
|
return (
|
||||||
<DyadThink
|
<DyadThink
|
||||||
|
|||||||
44
src/components/chat/DyadRead.tsx
Normal file
44
src/components/chat/DyadRead.tsx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import type React from "react";
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
import { FileText } from "lucide-react";
|
||||||
|
|
||||||
|
interface DyadReadProps {
|
||||||
|
children?: ReactNode;
|
||||||
|
node?: any;
|
||||||
|
path?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DyadRead: React.FC<DyadReadProps> = ({
|
||||||
|
children,
|
||||||
|
node,
|
||||||
|
path: pathProp,
|
||||||
|
}) => {
|
||||||
|
const path = pathProp || node?.properties?.path || "";
|
||||||
|
const fileName = path ? path.split("/").pop() : "";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-(--background-lightest) rounded-lg px-4 py-2 border border-border my-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<FileText size={16} className="text-gray-600" />
|
||||||
|
{fileName && (
|
||||||
|
<span className="text-gray-700 dark:text-gray-300 font-medium text-sm">
|
||||||
|
{fileName}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<div className="text-xs text-gray-600 font-medium">Read</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{path && (
|
||||||
|
<div className="text-xs text-gray-500 dark:text-gray-400 font-medium mb-1">
|
||||||
|
{path}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{children && (
|
||||||
|
<div className="text-sm text-gray-600 dark:text-gray-300 mt-2">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
31
src/components/chat/DyadWebSearch.tsx
Normal file
31
src/components/chat/DyadWebSearch.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import type React from "react";
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
import { Globe } from "lucide-react";
|
||||||
|
|
||||||
|
interface DyadWebSearchProps {
|
||||||
|
children?: ReactNode;
|
||||||
|
node?: any;
|
||||||
|
query?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DyadWebSearch: React.FC<DyadWebSearchProps> = ({
|
||||||
|
children,
|
||||||
|
node: _node,
|
||||||
|
query: queryProp,
|
||||||
|
}) => {
|
||||||
|
const query = queryProp || (typeof children === "string" ? children : "");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-(--background-lightest) rounded-lg px-4 py-2 border my-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Globe size={16} className="text-blue-600" />
|
||||||
|
<div className="text-xs text-blue-600 font-medium">Web Search</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm italic text-gray-600 dark:text-gray-300 mt-2">
|
||||||
|
{query || children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
78
src/components/chat/DyadWebSearchResult.tsx
Normal file
78
src/components/chat/DyadWebSearchResult.tsx
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { ChevronDown, ChevronUp, Globe, Loader } from "lucide-react";
|
||||||
|
import { VanillaMarkdownParser } from "./DyadMarkdownParser";
|
||||||
|
import { CustomTagState } from "./stateTypes";
|
||||||
|
|
||||||
|
interface DyadWebSearchResultProps {
|
||||||
|
node?: any;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DyadWebSearchResult: React.FC<DyadWebSearchResultProps> = ({
|
||||||
|
children,
|
||||||
|
node,
|
||||||
|
}) => {
|
||||||
|
const state = node?.properties?.state as CustomTagState;
|
||||||
|
const inProgress = state === "pending";
|
||||||
|
const [isExpanded, setIsExpanded] = useState(inProgress);
|
||||||
|
|
||||||
|
// Collapse when transitioning from in-progress to not-in-progress
|
||||||
|
useEffect(() => {
|
||||||
|
if (!inProgress && isExpanded) {
|
||||||
|
setIsExpanded(false);
|
||||||
|
}
|
||||||
|
}, [inProgress]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`relative bg-(--background-lightest) dark:bg-zinc-900 hover:bg-(--background-lighter) rounded-lg px-4 py-2 border my-2 cursor-pointer ${
|
||||||
|
inProgress ? "border-blue-500" : "border-border"
|
||||||
|
}`}
|
||||||
|
onClick={() => setIsExpanded(!isExpanded)}
|
||||||
|
role="button"
|
||||||
|
aria-expanded={isExpanded}
|
||||||
|
tabIndex={0}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsExpanded(!isExpanded);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Top-left label badge */}
|
||||||
|
<div
|
||||||
|
className="absolute top-2 left-2 flex items-center gap-1 px-2 py-0.5 rounded text-xs font-semibold text-blue-600 bg-white dark:bg-zinc-900"
|
||||||
|
style={{ zIndex: 1 }}
|
||||||
|
>
|
||||||
|
<Globe size={16} className="text-blue-600" />
|
||||||
|
<span>Web Search Result</span>
|
||||||
|
{inProgress && (
|
||||||
|
<Loader size={14} className="ml-1 text-blue-600 animate-spin" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Indicator icon */}
|
||||||
|
<div className="absolute top-2 right-2 p-1 text-gray-500">
|
||||||
|
{isExpanded ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main content with smooth transition */}
|
||||||
|
<div
|
||||||
|
className="pt-6 overflow-hidden transition-all duration-300 ease-in-out"
|
||||||
|
style={{
|
||||||
|
maxHeight: isExpanded ? "none" : "0px",
|
||||||
|
opacity: isExpanded ? 1 : 0,
|
||||||
|
marginBottom: isExpanded ? "0" : "-6px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="px-0 text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
{typeof children === "string" ? (
|
||||||
|
<VanillaMarkdownParser content={children} />
|
||||||
|
) : (
|
||||||
|
children
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -86,7 +86,8 @@ export async function getModelClient(
|
|||||||
if (providerConfig.gatewayPrefix != null || dyadEngineUrl) {
|
if (providerConfig.gatewayPrefix != null || dyadEngineUrl) {
|
||||||
const isEngineEnabled =
|
const isEngineEnabled =
|
||||||
settings.enableProSmartFilesContextMode ||
|
settings.enableProSmartFilesContextMode ||
|
||||||
settings.enableProLazyEditsMode;
|
settings.enableProLazyEditsMode ||
|
||||||
|
settings.enableProWebSearch;
|
||||||
const provider = isEngineEnabled
|
const provider = isEngineEnabled
|
||||||
? createDyadEngine({
|
? createDyadEngine({
|
||||||
apiKey: dyadApiKey,
|
apiKey: dyadApiKey,
|
||||||
@@ -100,6 +101,7 @@ export async function getModelClient(
|
|||||||
enableSmartFilesContext: settings.enableProSmartFilesContextMode,
|
enableSmartFilesContext: settings.enableProSmartFilesContextMode,
|
||||||
// Keep in sync with getCurrentValue in ProModeSelector.tsx
|
// Keep in sync with getCurrentValue in ProModeSelector.tsx
|
||||||
smartContextMode: settings.proSmartContextOption ?? "balanced",
|
smartContextMode: settings.proSmartContextOption ?? "balanced",
|
||||||
|
enableWebSearch: settings.enableProWebSearch,
|
||||||
},
|
},
|
||||||
settings,
|
settings,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ or to provide a custom fetch implementation for e.g. testing.
|
|||||||
dyadOptions: {
|
dyadOptions: {
|
||||||
enableLazyEdits?: boolean;
|
enableLazyEdits?: boolean;
|
||||||
enableSmartFilesContext?: boolean;
|
enableSmartFilesContext?: boolean;
|
||||||
|
enableWebSearch?: boolean;
|
||||||
smartContextMode?: "balanced" | "conservative";
|
smartContextMode?: "balanced" | "conservative";
|
||||||
};
|
};
|
||||||
settings: UserSettings;
|
settings: UserSettings;
|
||||||
@@ -158,6 +159,7 @@ export function createDyadEngine(
|
|||||||
enable_smart_files_context:
|
enable_smart_files_context:
|
||||||
options.dyadOptions.enableSmartFilesContext,
|
options.dyadOptions.enableSmartFilesContext,
|
||||||
smart_context_mode: options.dyadOptions.smartContextMode,
|
smart_context_mode: options.dyadOptions.smartContextMode,
|
||||||
|
enable_web_search: options.dyadOptions.enableWebSearch,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -219,6 +219,7 @@ export const UserSettingsSchema = z.object({
|
|||||||
thinkingBudget: z.enum(["low", "medium", "high"]).optional(),
|
thinkingBudget: z.enum(["low", "medium", "high"]).optional(),
|
||||||
enableProLazyEditsMode: z.boolean().optional(),
|
enableProLazyEditsMode: z.boolean().optional(),
|
||||||
enableProSmartFilesContextMode: z.boolean().optional(),
|
enableProSmartFilesContextMode: z.boolean().optional(),
|
||||||
|
enableProWebSearch: z.boolean().optional(),
|
||||||
proSmartContextOption: z.enum(["balanced", "conservative"]).optional(),
|
proSmartContextOption: z.enum(["balanced", "conservative"]).optional(),
|
||||||
selectedTemplateId: z.string(),
|
selectedTemplateId: z.string(),
|
||||||
enableSupabaseWriteSqlMigration: z.boolean().optional(),
|
enableSupabaseWriteSqlMigration: z.boolean().optional(),
|
||||||
|
|||||||
Reference in New Issue
Block a user