Compare commits

...

11 Commits

Author SHA1 Message Date
xing.liu d0c6f450d4 feat(ut): add ut coverage check 2025-05-15 23:21:55 +08:00
Leo Hui a43db94fb6 feat: refactor crawler trust link style (#166)
* feat: refactor crawler trust link style

* feat: enhance link credibility checks in Markdown and related components
2025-05-15 17:17:10 +08:00
JeffJiang 8802eea0ba fix: report editor styles (#163)
* fix: report editor styles
2025-05-15 15:18:01 +08:00
Leo Hui 1a59accb52 fix: adjust slider width for responsive design in multi-agent visualization (#134) 2025-05-15 11:59:16 +08:00
JeffJiang 86295ed195 fix: hallucination link warn (#158) 2025-05-15 10:58:24 +08:00
JeffJiang bf4820c68f Check the output links are hallucinations from AI (#139)
* feat: check output links if a hallucination from AI
2025-05-15 10:39:53 +08:00
Abeautifulsnow 25e7b86f02 optimize docker backend image size (#130) 2025-05-15 09:52:14 +08:00
Maxim Kot 0459e3c9f8 Update README.md (#122)
Fixed the link to the configuration from the docker section
2025-05-15 08:54:33 +08:00
DanielWalnut 5cc0e61297 refactor: refine the step execute human message (#144) 2025-05-14 18:54:14 +08:00
Henry Li a220f4b6ea feat: add python result and error handling (#141) 2025-05-14 03:47:28 -07:00
DanielWalnut f73a7a229c refactor: add existing research findings into step human message (#140) 2025-05-14 18:40:14 +08:00
19 changed files with 375 additions and 47 deletions
+18 -2
View File
@@ -23,7 +23,23 @@ jobs:
uv pip install -e ".[dev]" uv pip install -e ".[dev]"
uv pip install -e ".[test]" uv pip install -e ".[test]"
- name: Run test cases - name: Run test cases with coverage
run: | run: |
source .venv/bin/activate source .venv/bin/activate
TAVILY_API_KEY=mock-key make test TAVILY_API_KEY=mock-key make coverage
- name: Generate HTML Coverage Report
run: |
source .venv/bin/activate
python -m coverage html -d coverage_html
- name: Upload Coverage Report
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage_html/
- name: Display Coverage Summary
run: |
source .venv/bin/activate
python -m coverage report
+3
View File
@@ -21,3 +21,6 @@ conf.yaml
.idea/ .idea/
.langgraph_api/ .langgraph_api/
# coverage report
coverage.xml
coverage/
+1 -1
View File
@@ -1,4 +1,4 @@
FROM ghcr.io/astral-sh/uv:python3.12-bookworm FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim
# Install uv. # Install uv.
COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv
+1 -1
View File
@@ -19,4 +19,4 @@ langgraph-dev:
uvx --refresh --from "langgraph-cli[inmem]" --with-editable . --python 3.12 langgraph dev --allow-blocking uvx --refresh --from "langgraph-cli[inmem]" --with-editable . --python 3.12 langgraph dev --allow-blocking
coverage: coverage:
uv run pytest --cov=src tests/ --cov-report=term-missing uv run pytest --cov=src tests/ --cov-report=term-missing --cov-report=xml
+2 -2
View File
@@ -2,7 +2,7 @@
[![Python 3.12+](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/downloads/) [![Python 3.12+](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/downloads/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![DeepWiki](https://img.shields.io/badge/DeepWiki-bytedance%2Fdeer--flow-blue.svg?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAyCAYAAAAnWDnqAAAAAXNSR0IArs4c6QAAA05JREFUaEPtmUtyEzEQhtWTQyQLHNak2AB7ZnyXZMEjXMGeK/AIi+QuHrMnbChYY7MIh8g01fJoopFb0uhhEqqcbWTp06/uv1saEDv4O3n3dV60RfP947Mm9/SQc0ICFQgzfc4CYZoTPAswgSJCCUJUnAAoRHOAUOcATwbmVLWdGoH//PB8mnKqScAhsD0kYP3j/Yt5LPQe2KvcXmGvRHcDnpxfL2zOYJ1mFwrryWTz0advv1Ut4CJgf5uhDuDj5eUcAUoahrdY/56ebRWeraTjMt/00Sh3UDtjgHtQNHwcRGOC98BJEAEymycmYcWwOprTgcB6VZ5JK5TAJ+fXGLBm3FDAmn6oPPjR4rKCAoJCal2eAiQp2x0vxTPB3ALO2CRkwmDy5WohzBDwSEFKRwPbknEggCPB/imwrycgxX2NzoMCHhPkDwqYMr9tRcP5qNrMZHkVnOjRMWwLCcr8ohBVb1OMjxLwGCvjTikrsBOiA6fNyCrm8V1rP93iVPpwaE+gO0SsWmPiXB+jikdf6SizrT5qKasx5j8ABbHpFTx+vFXp9EnYQmLx02h1QTTrl6eDqxLnGjporxl3NL3agEvXdT0WmEost648sQOYAeJS9Q7bfUVoMGnjo4AZdUMQku50McDcMWcBPvr0SzbTAFDfvJqwLzgxwATnCgnp4wDl6Aa+Ax283gghmj+vj7feE2KBBRMW3FzOpLOADl0Isb5587h/U4gGvkt5v60Z1VLG8BhYjbzRwyQZemwAd6cCR5/XFWLYZRIMpX39AR0tjaGGiGzLVyhse5C9RKC6ai42ppWPKiBagOvaYk8lO7DajerabOZP46Lby5wKjw1HCRx7p9sVMOWGzb/vA1hwiWc6jm3MvQDTogQkiqIhJV0nBQBTU+3okKCFDy9WwferkHjtxib7t3xIUQtHxnIwtx4mpg26/HfwVNVDb4oI9RHmx5WGelRVlrtiw43zboCLaxv46AZeB3IlTkwouebTr1y2NjSpHz68WNFjHvupy3q8TFn3Hos2IAk4Ju5dCo8B3wP7VPr/FGaKiG+T+v+TQqIrOqMTL1VdWV1DdmcbO8KXBz6esmYWYKPwDL5b5FA1a0hwapHiom0r/cKaoqr+27/XcrS5UwSMbQAAAABJRU5ErkJggg==)](https://deepwiki.com/bytedance/deer-flow) [![DeepWiki](https://img.shields.io/badge/DeepWiki-bytedance%2Fdeer--flow-blue.svg?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAyCAYAAAAnWDnqAAAAAXNSR0IArs4c6QAAA05JREFUaEPtmUtyEzEQhtWTQyQLHNak2AB7ZnyXZMEjXMGeK/AIi+QuHrMnbChYY7MIh8g01fJoopFb0uhhEqqcbWTp06/uv1saEDv4O3n3dV60RfP947Mm9/SQc0ICFQgzfc4CYZoTPAswgSJCCUJUnAAoRHOAUOcATwbmVLWdGoH//PB8mnKqScAhsD0kYP3j/Yt5LPQe2KvcXmGvRHcDnpxfL2zOYJ1mFwrryWTz0advv1Ut4CJgf5uhDuDj5eUcAUoahrdY/56ebRWeraTjMt/00Sh3UDtjgHtQNHwcRGOC98BJEAEymycmYcWwOprTgcB6VZ5JK5TAJ+fXGLBm3FDAmn6oPPjR4rKCAoJCal2eAiQp2x0vxTPB3ALO2CRkwmDy5WohzBDwSEFKRwPbknEggCPB/imwrycgxX2NzoMCHhPkDwqYMr9tRcP5qNrMZHkVnOjRMWwLCcr8ohBVb1OMjxLwGCvjTikrsBOiA6fNyCrm8V1rP93iVPpwaE+gO0SsWmPiXB+jikdf6SizrT5qKasx5j8ABbHpFTx+vFXp9EnYQmLx02h1QTTrl6eDqxLnGjporxl3NL3agEvXdT0WmEost648sQOYAeJS9Q7bfUVoMGnjo4AZdUMQku50McCcMWcBPvr0SzbTAFDfvJqwLzgxwATnCgnp4wDl6Aa+Ax283gghmj+vj7feE2KBBRMW3FzOpLOADl0Isb5587h/U4gGvkt5v60Z1VLG8BhYjbzRwyQZemwAd6cCR5/XFWLYZRIMpX39AR0tjaGGiGzLVyhse5C9RKC6ai42ppWPKiBagOvaYk8lO7DajerabOZP46Lby5wKjw1HCRx7p9sVMOWGzb/vA1hwiWc6jm3MvQDTogQkiqIhJV0nBQBTU+3okKCFDy9WwferkHjtxib7t3xIUQtHxnIwtx4mpg26/HfwVNVDb4oI9RHmx5WGelRVlrtiw43zboCLaxv46AZeB3IlTkwouebTr1y2NjSpHz68WNFjHvupy3q8TFn3Hos2IAk4Ju5dCo8B3wP7VPr/FGaKiG+T+v+TQqIrOqMTL1VdWV1DdmcbO8KXBz6esmYWYKPwDL5b5FA1a0hwapHiom0r/cKaoqr+27/XcrS5UwSMbQAAAABJRU5ErkJggg==)](https://deepwiki.com/bytedance/deer-flow)
<!-- DeepWiki badge generated by https://deepwiki.ryoppippi.com/ --> <!-- DeepWiki badge generated by https://deepwiki.ryoppippi.com/ -->
@@ -370,7 +370,7 @@ This will enable trace visualization in LangGraph Studio and send your traces to
You can also run this project with Docker. You can also run this project with Docker.
First, you need read the [configuration](#configuration) below. Make sure `.env`, `.conf.yaml` files are ready. First, you need read the [configuration](docs/configuration_guide.md) below. Make sure `.env`, `.conf.yaml` files are ready.
Second, to build a Docker image of your own web server: Second, to build a Docker image of your own web server:
+3
View File
@@ -52,6 +52,9 @@ filterwarnings = [
"ignore::UserWarning", "ignore::UserWarning",
] ]
[tool.coverage.report]
fail_under = 25
[tool.hatch.build.targets.wheel] [tool.hatch.build.targets.wheel]
packages = ["src"] packages = ["src"]
+22 -5
View File
@@ -308,17 +308,34 @@ async def _execute_agent_step(
observations = state.get("observations", []) observations = state.get("observations", [])
# Find the first unexecuted step # Find the first unexecuted step
current_step = None
completed_steps = []
for step in current_plan.steps: for step in current_plan.steps:
if not step.execution_res: if not step.execution_res:
current_step = step
break break
else:
completed_steps.append(step)
logger.info(f"Executing step: {step.title}") if not current_step:
logger.warning("No unexecuted step found")
return Command(goto="research_team")
# Prepare the input for the agent logger.info(f"Executing step: {current_step.title}")
# Format completed steps information
completed_steps_info = ""
if completed_steps:
completed_steps_info = "# Existing Research Findings\n\n"
for i, step in enumerate(completed_steps):
completed_steps_info += f"## Existing Finding {i+1}: {step.title}\n\n"
completed_steps_info += f"<finding>\n{step.execution_res}\n</finding>\n\n"
# Prepare the input for the agent with completed steps info
agent_input = { agent_input = {
"messages": [ "messages": [
HumanMessage( HumanMessage(
content=f"#Task\n\n##title\n\n{step.title}\n\n##description\n\n{step.description}\n\n##locale\n\n{state.get('locale', 'en-US')}" content=f"{completed_steps_info}# Current Task\n\n## Title\n\n{current_step.title}\n\n## Description\n\n{current_step.description}\n\n## Locale\n\n{state.get('locale', 'en-US')}"
) )
] ]
} }
@@ -340,8 +357,8 @@ async def _execute_agent_step(
logger.debug(f"{agent_name.capitalize()} full response: {response_content}") logger.debug(f"{agent_name.capitalize()} full response: {response_content}")
# Update the step with the execution result # Update the step with the execution result
step.execution_res = response_content current_step.execution_res = response_content
logger.info(f"Step '{step.title}' execution completed by {agent_name}") logger.info(f"Step '{current_step.title}' execution completed by {agent_name}")
return Command( return Command(
update={ update={
+24
View File
@@ -0,0 +1,24 @@
#!/usr/bin/env python3
"""
This script manually patches sys.modules to fix the LLM import issue
so that tests can run without requiring LLM configuration.
"""
import sys
from unittest.mock import MagicMock
# Create mocks
mock_llm = MagicMock()
mock_llm.invoke.return_value = "Mock LLM response"
# Create a mock module for llm.py
mock_llm_module = MagicMock()
mock_llm_module.get_llm_by_type = lambda llm_type: mock_llm
mock_llm_module.basic_llm = mock_llm
mock_llm_module._create_llm_use_conf = lambda llm_type, conf: mock_llm
# Set the mock module
sys.modules["src.llms.llm"] = mock_llm_module
print("Successfully patched LLM module. You can now run your tests.")
print("Example: uv run pytest tests/test_types.py -v")
+131
View File
@@ -0,0 +1,131 @@
import pytest
import sys
import os
from typing import Annotated, List, Optional
# Import MessagesState directly from langgraph rather than through our application
from langgraph.graph import MessagesState
# Create stub versions of Plan/Step/StepType to avoid dependencies
class StepType:
RESEARCH = "research"
PROCESSING = "processing"
class Step:
def __init__(self, need_web_search, title, description, step_type):
self.need_web_search = need_web_search
self.title = title
self.description = description
self.step_type = step_type
class Plan:
def __init__(self, locale, has_enough_context, thought, title, steps):
self.locale = locale
self.has_enough_context = has_enough_context
self.thought = thought
self.title = title
self.steps = steps
# Import the actual State class by loading the module directly
# This avoids the cascade of imports that would normally happen
def load_state_class():
# Get the absolute path to the types.py file
src_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src"))
types_path = os.path.join(src_dir, "graph", "types.py")
# Create a namespace for the module
import types
module_name = "src.graph.types_direct"
spec = types.ModuleType(module_name)
# Add the module to sys.modules to avoid import loops
sys.modules[module_name] = spec
# Set up the namespace with required imports
spec.__dict__["operator"] = __import__("operator")
spec.__dict__["Annotated"] = Annotated
spec.__dict__["MessagesState"] = MessagesState
spec.__dict__["Plan"] = Plan
# Execute the module code
with open(types_path, "r") as f:
module_code = f.read()
exec(module_code, spec.__dict__)
# Return the State class
return spec.State
# Load the actual State class
State = load_state_class()
def test_state_initialization():
"""Test that State class has correct default attribute definitions."""
# Test that the class has the expected attribute definitions
assert State.locale == "en-US"
assert State.observations == []
assert State.plan_iterations == 0
assert State.current_plan is None
assert State.final_report == ""
assert State.auto_accepted_plan is False
assert State.enable_background_investigation is True
assert State.background_investigation_results is None
# Verify state initialization
state = State(messages=[])
assert "messages" in state
# Without explicitly passing attributes, they're not in the state
assert "locale" not in state
assert "observations" not in state
def test_state_with_custom_values():
"""Test that State can be initialized with custom values."""
test_step = Step(
need_web_search=True,
title="Test Step",
description="Step description",
step_type=StepType.RESEARCH,
)
test_plan = Plan(
locale="en-US",
has_enough_context=False,
thought="Test thought",
title="Test Plan",
steps=[test_step],
)
# Initialize state with custom values and required messages field
state = State(
messages=[],
locale="fr-FR",
observations=["Observation 1"],
plan_iterations=2,
current_plan=test_plan,
final_report="Test report",
auto_accepted_plan=True,
enable_background_investigation=False,
background_investigation_results="Test results",
)
# Access state keys - these are explicitly initialized
assert state["locale"] == "fr-FR"
assert state["observations"] == ["Observation 1"]
assert state["plan_iterations"] == 2
assert state["current_plan"].title == "Test Plan"
assert state["current_plan"].thought == "Test thought"
assert len(state["current_plan"].steps) == 1
assert state["current_plan"].steps[0].title == "Test Step"
assert state["final_report"] == "Test report"
assert state["auto_accepted_plan"] is True
assert state["enable_background_investigation"] is False
assert state["background_investigation_results"] == "Test results"
@@ -75,7 +75,9 @@ function ActivityMessage({ messageId }: { messageId: string }) {
if (message.agent !== "reporter" && message.agent !== "planner") { if (message.agent !== "reporter" && message.agent !== "planner") {
return ( return (
<div className="px-4 py-2"> <div className="px-4 py-2">
<Markdown animated>{message.content}</Markdown> <Markdown animated checkLinkCredibility>
{message.content}
</Markdown>
</div> </div>
); );
} }
@@ -304,10 +306,58 @@ function PythonToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
</SyntaxHighlighter> </SyntaxHighlighter>
</div> </div>
</div> </div>
{toolCall.result && <PythonToolCallResult result={toolCall.result} />}
</section> </section>
); );
} }
function PythonToolCallResult({ result }: { result: string }) {
const { resolvedTheme } = useTheme();
const hasError = useMemo(
() => result.includes("Error executing code:\n"),
[result],
);
const error = useMemo(() => {
if (hasError) {
const parts = result.split("```\nError: ");
if (parts.length > 1) {
return parts[1]!.trim();
}
}
return null;
}, [result, hasError]);
const stdout = useMemo(() => {
if (!hasError) {
const parts = result.split("```\nStdout: ");
if (parts.length > 1) {
return parts[1]!.trim();
}
}
return null;
}, [result, hasError]);
return (
<>
<div className="mt-4 font-medium italic">
{hasError ? "Error when executing the above code" : "Execution output"}
</div>
<div className="bg-accent mt-2 max-h-[400px] max-w-[calc(100%-120px)] overflow-y-auto rounded-md p-2 text-sm">
<SyntaxHighlighter
language="plaintext"
style={resolvedTheme === "dark" ? dark : docco}
customStyle={{
color: hasError ? "red" : "inherit",
background: "transparent",
border: "none",
boxShadow: "none",
}}
>
{error ?? stdout ?? "(empty)"}
</SyntaxHighlighter>
</div>
</>
);
}
function MCPToolCall({ toolCall }: { toolCall: ToolCallRuntime }) { function MCPToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
const tool = useMemo(() => findMCPTool(toolCall.name), [toolCall.name]); const tool = useMemo(() => findMCPTool(toolCall.name), [toolCall.name]);
const { resolvedTheme } = useTheme(); const { resolvedTheme } = useTheme();
+18 -1
View File
@@ -1,7 +1,7 @@
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates // Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import { Check, Copy, Headphones, X } from "lucide-react"; import { Check, Copy, Headphones, Pencil, Undo2, X } from "lucide-react";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { ScrollContainer } from "~/components/deer-flow/scroll-container"; import { ScrollContainer } from "~/components/deer-flow/scroll-container";
@@ -47,6 +47,7 @@ export function ResearchBlock({
await listenToPodcast(researchId); await listenToPodcast(researchId);
}, [researchId]); }, [researchId]);
const [editing, setEditing] = useState(false);
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
const handleCopy = useCallback(() => { const handleCopy = useCallback(() => {
if (!reportId) { if (!reportId) {
@@ -63,6 +64,10 @@ export function ResearchBlock({
}, 1000); }, 1000);
}, [reportId]); }, [reportId]);
const handleEdit = useCallback(() => {
setEditing((editing) => !editing);
}, []);
// When the research id changes, set the active tab to activities // When the research id changes, set the active tab to activities
useEffect(() => { useEffect(() => {
if (!hasReport) { if (!hasReport) {
@@ -87,6 +92,17 @@ export function ResearchBlock({
<Headphones /> <Headphones />
</Button> </Button>
</Tooltip> </Tooltip>
<Tooltip title="Edit">
<Button
className="text-gray-400"
size="icon"
variant="ghost"
disabled={isReplay}
onClick={handleEdit}
>
{editing ? <Undo2 /> : <Pencil />}
</Button>
</Tooltip>
<Tooltip title="Copy"> <Tooltip title="Copy">
<Button <Button
className="text-gray-400" className="text-gray-400"
@@ -147,6 +163,7 @@ export function ResearchBlock({
className="mt-4" className="mt-4"
researchId={researchId} researchId={researchId}
messageId={reportId} messageId={reportId}
editing={editing}
/> />
)} )}
</ScrollContainer> </ScrollContainer>
@@ -13,10 +13,12 @@ import { cn } from "~/lib/utils";
export function ResearchReportBlock({ export function ResearchReportBlock({
className, className,
messageId, messageId,
editing,
}: { }: {
className?: string; className?: string;
researchId: string; researchId: string;
messageId: string; messageId: string;
editing: boolean;
}) { }) {
const message = useMessage(messageId); const message = useMessage(messageId);
const { isReplay } = useReplay(); const { isReplay } = useReplay();
@@ -55,14 +57,16 @@ export function ResearchReportBlock({
ref={contentRef} ref={contentRef}
className={cn("relative flex flex-col pt-4 pb-8", className)} className={cn("relative flex flex-col pt-4 pb-8", className)}
> >
{!isReplay && isCompleted ? ( {!isReplay && isCompleted && editing ? (
<ReportEditor <ReportEditor
content={message?.content} content={message?.content}
onMarkdownChange={handleMarkdownChange} onMarkdownChange={handleMarkdownChange}
/> />
) : ( ) : (
<> <>
<Markdown animated>{message?.content}</Markdown> <Markdown animated checkLinkCredibility>
{message?.content}
</Markdown>
{message?.isStreaming && <LoadingAnimation className="my-12" />} {message?.isStreaming && <LoadingAnimation className="my-12" />}
</> </>
)} )}
@@ -147,7 +147,7 @@ export function MultiAgentVisualization({ className }: { className?: string }) {
</Tooltip> </Tooltip>
<div className="text-muted-foreground ml-2 flex items-center justify-center"> <div className="text-muted-foreground ml-2 flex items-center justify-center">
<Slider <Slider
className="w-120" className="w-40 sm:w-80 md:w-100 lg:w-120"
max={playbook.steps.length - 1} max={playbook.steps.length - 1}
min={0} min={0}
step={1} step={1}
+54
View File
@@ -0,0 +1,54 @@
import { useMemo } from "react";
import { useStore, useToolCalls } from "~/core/store";
import { Tooltip } from "./tooltip";
import { WarningFilled } from "@ant-design/icons";
export const Link = ({
href,
children,
checkLinkCredibility = false,
}: {
href: string | undefined;
children: React.ReactNode;
checkLinkCredibility: boolean;
}) => {
const toolCalls = useToolCalls();
const responding = useStore((state) => state.responding);
const credibleLinks = useMemo(() => {
const links = new Set<string>();
if (!checkLinkCredibility) return links;
(toolCalls || []).forEach((call) => {
if (call && call.name === "web_search" && call.result) {
const result = JSON.parse(call.result) as Array<{ url: string }>;
result.forEach((r) => {
links.add(r.url);
});
}
});
return links;
}, [toolCalls]);
const isCredible = useMemo(() => {
return checkLinkCredibility && href && !responding
? credibleLinks.has(href)
: true;
}, [credibleLinks, href, responding, checkLinkCredibility]);
return (
<span className="flex items-center gap-1.5">
<a href={href} target="_blank" rel="noopener noreferrer">
{children}
</a>
{!isCredible && (
<Tooltip
title="This link might be a hallucination from AI model and may not be reliable."
delayDuration={300}
>
<WarningFilled className="text-sx transition-colors hover:!text-yellow-500" />
</Tooltip>
)}
</span>
);
};
+19 -20
View File
@@ -18,19 +18,7 @@ import { cn } from "~/lib/utils";
import Image from "./image"; import Image from "./image";
import { Tooltip } from "./tooltip"; import { Tooltip } from "./tooltip";
import { Link } from "./link";
const components: ReactMarkdownOptions["components"] = {
a: ({ href, children }) => (
<a href={href} target="_blank" rel="noopener noreferrer">
{children}
</a>
),
img: ({ src, alt }) => (
<a href={src as string} target="_blank" rel="noopener noreferrer">
<Image className="rounded" src={src as string} alt={alt ?? ""} />
</a>
),
};
export function Markdown({ export function Markdown({
className, className,
@@ -38,13 +26,30 @@ export function Markdown({
style, style,
enableCopy, enableCopy,
animated = false, animated = false,
checkLinkCredibility = false,
...props ...props
}: ReactMarkdownOptions & { }: ReactMarkdownOptions & {
className?: string; className?: string;
enableCopy?: boolean; enableCopy?: boolean;
style?: React.CSSProperties; style?: React.CSSProperties;
animated?: boolean; animated?: boolean;
checkLinkCredibility?: boolean;
}) { }) {
const components: ReactMarkdownOptions["components"] = useMemo(() => {
return {
a: ({ href, children }) => (
<Link href={href} checkLinkCredibility={checkLinkCredibility}>
{children}
</Link>
),
img: ({ src, alt }) => (
<a href={src as string} target="_blank" rel="noopener noreferrer">
<Image className="rounded" src={src as string} alt={alt ?? ""} />
</a>
),
};
}, [checkLinkCredibility]);
const rehypePlugins = useMemo(() => { const rehypePlugins = useMemo(() => {
if (animated) { if (animated) {
return [rehypeKatex, rehypeSplitWordsIntoSpans]; return [rehypeKatex, rehypeSplitWordsIntoSpans];
@@ -52,13 +57,7 @@ export function Markdown({
return [rehypeKatex]; return [rehypeKatex];
}, [animated]); }, [animated]);
return ( return (
<div <div className={cn(className, "prose dark:prose-invert")} style={style}>
className={cn(
className,
"prose dark:prose-invert prose-p:my-0 prose-img:mt-0 flex flex-col gap-4",
)}
style={style}
>
<ReactMarkdown <ReactMarkdown
remarkPlugins={[remarkGfm, remarkMath]} remarkPlugins={[remarkGfm, remarkMath]}
rehypePlugins={rehypePlugins} rehypePlugins={rehypePlugins}
+3 -1
View File
@@ -19,6 +19,7 @@ export function Tooltip({
open, open,
side, side,
sideOffset, sideOffset,
delayDuration = 750,
}: { }: {
className?: string; className?: string;
style?: CSSProperties; style?: CSSProperties;
@@ -27,10 +28,11 @@ export function Tooltip({
open?: boolean; open?: boolean;
side?: "left" | "right" | "top" | "bottom"; side?: "left" | "right" | "top" | "bottom";
sideOffset?: number; sideOffset?: number;
delayDuration?: number;
}) { }) {
return ( return (
<TooltipProvider> <TooltipProvider>
<ShadcnTooltip delayDuration={750} open={open}> <ShadcnTooltip delayDuration={delayDuration} open={open}>
<TooltipTrigger asChild>{children}</TooltipTrigger> <TooltipTrigger asChild>{children}</TooltipTrigger>
<TooltipContent <TooltipContent
className={cn(className)} className={cn(className)}
+3 -10
View File
@@ -78,16 +78,12 @@ const taskItem = TaskItem.configure({
}); });
const horizontalRule = HorizontalRule.configure({ const horizontalRule = HorizontalRule.configure({
HTMLAttributes: { HTMLAttributes: {},
class: cx("mt-4 mb-6 border-t border-muted-foreground"),
},
}); });
const starterKit = StarterKit.configure({ const starterKit = StarterKit.configure({
bulletList: { bulletList: {
HTMLAttributes: { HTMLAttributes: {},
class: cx("list-disc list-outside leading-3 -mt-2"),
},
}, },
orderedList: { orderedList: {
HTMLAttributes: { HTMLAttributes: {
@@ -95,9 +91,7 @@ const starterKit = StarterKit.configure({
}, },
}, },
listItem: { listItem: {
HTMLAttributes: { HTMLAttributes: {},
class: cx("leading-normal -mb-2"),
},
}, },
blockquote: { blockquote: {
HTMLAttributes: { HTMLAttributes: {
@@ -107,7 +101,6 @@ const starterKit = StarterKit.configure({
codeBlock: false, codeBlock: false,
code: { code: {
HTMLAttributes: { HTMLAttributes: {
class: cx("rounded-md bg-muted px-1.5 py-1 font-mono font-medium"),
spellcheck: "false", spellcheck: "false",
}, },
}, },
+11
View File
@@ -377,3 +377,14 @@ export function useLastFeedbackMessageId() {
); );
return waitingForFeedbackMessageId; return waitingForFeedbackMessageId;
} }
export function useToolCalls() {
return useStore(
useShallow((state) => {
return state.messageIds
?.map((id) => getMessage(id)?.toolCalls)
.filter((toolCalls) => toolCalls != null)
.flat();
}),
);
}
+4
View File
@@ -4,6 +4,10 @@
color: inherit; color: inherit;
} }
.ProseMirror {
line-height: 1.75;
}
.ProseMirror .is-editor-empty:first-child::before { .ProseMirror .is-editor-empty:first-child::before {
content: attr(data-placeholder); content: attr(data-placeholder);
float: left; float: left;