Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3131f961a4 |
@@ -23,23 +23,7 @@ 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 with coverage
|
- name: Run test cases
|
||||||
run: |
|
run: |
|
||||||
source .venv/bin/activate
|
source .venv/bin/activate
|
||||||
TAVILY_API_KEY=mock-key make coverage
|
TAVILY_API_KEY=mock-key make test
|
||||||
|
|
||||||
- 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
|
|
||||||
@@ -21,6 +21,3 @@ conf.yaml
|
|||||||
.idea/
|
.idea/
|
||||||
.langgraph_api/
|
.langgraph_api/
|
||||||
|
|
||||||
# coverage report
|
|
||||||
coverage.xml
|
|
||||||
coverage/
|
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim
|
FROM ghcr.io/astral-sh/uv:python3.12-bookworm
|
||||||
|
|
||||||
# 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
|
||||||
|
|||||||
@@ -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 --cov-report=xml
|
uv run pytest --cov=src tests/ --cov-report=term-missing
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
[](https://www.python.org/downloads/)
|
[](https://www.python.org/downloads/)
|
||||||
[](https://opensource.org/licenses/MIT)
|
[](https://opensource.org/licenses/MIT)
|
||||||
[](https://deepwiki.com/bytedance/deer-flow)
|
[](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](docs/configuration_guide.md) below. Make sure `.env`, `.conf.yaml` files are ready.
|
First, you need read the [configuration](#configuration) 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:
|
||||||
|
|
||||||
|
|||||||
@@ -52,9 +52,6 @@ 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"]
|
||||||
|
|
||||||
|
|||||||
+5
-22
@@ -308,34 +308,17 @@ 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)
|
|
||||||
|
|
||||||
if not current_step:
|
logger.info(f"Executing step: {step.title}")
|
||||||
logger.warning("No unexecuted step found")
|
|
||||||
return Command(goto="research_team")
|
|
||||||
|
|
||||||
logger.info(f"Executing step: {current_step.title}")
|
# Prepare the input for the agent
|
||||||
|
|
||||||
# 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"{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')}"
|
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')}"
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -357,8 +340,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
|
||||||
current_step.execution_res = response_content
|
step.execution_res = response_content
|
||||||
logger.info(f"Step '{current_step.title}' execution completed by {agent_name}")
|
logger.info(f"Step '{step.title}' execution completed by {agent_name}")
|
||||||
|
|
||||||
return Command(
|
return Command(
|
||||||
update={
|
update={
|
||||||
|
|||||||
-24
@@ -1,24 +0,0 @@
|
|||||||
#!/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")
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
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,9 +75,7 @@ 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 checkLinkCredibility>
|
<Markdown animated>{message.content}</Markdown>
|
||||||
{message.content}
|
|
||||||
</Markdown>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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, Pencil, Undo2, X } from "lucide-react";
|
import { Check, Copy, Headphones, 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,7 +47,6 @@ 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) {
|
||||||
@@ -64,10 +63,6 @@ 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) {
|
||||||
@@ -92,17 +87,6 @@ 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"
|
||||||
@@ -163,7 +147,6 @@ export function ResearchBlock({
|
|||||||
className="mt-4"
|
className="mt-4"
|
||||||
researchId={researchId}
|
researchId={researchId}
|
||||||
messageId={reportId}
|
messageId={reportId}
|
||||||
editing={editing}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</ScrollContainer>
|
</ScrollContainer>
|
||||||
|
|||||||
@@ -13,12 +13,10 @@ 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();
|
||||||
@@ -57,16 +55,14 @@ 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 && editing ? (
|
{!isReplay && isCompleted ? (
|
||||||
<ReportEditor
|
<ReportEditor
|
||||||
content={message?.content}
|
content={message?.content}
|
||||||
onMarkdownChange={handleMarkdownChange}
|
onMarkdownChange={handleMarkdownChange}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Markdown animated checkLinkCredibility>
|
<Markdown animated>{message?.content}</Markdown>
|
||||||
{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-40 sm:w-80 md:w-100 lg:w-120"
|
className="w-120"
|
||||||
max={playbook.steps.length - 1}
|
max={playbook.steps.length - 1}
|
||||||
min={0}
|
min={0}
|
||||||
step={1}
|
step={1}
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -18,7 +18,19 @@ 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,
|
||||||
@@ -26,30 +38,13 @@ 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];
|
||||||
@@ -57,7 +52,13 @@ export function Markdown({
|
|||||||
return [rehypeKatex];
|
return [rehypeKatex];
|
||||||
}, [animated]);
|
}, [animated]);
|
||||||
return (
|
return (
|
||||||
<div className={cn(className, "prose dark:prose-invert")} style={style}>
|
<div
|
||||||
|
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}
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ export function Tooltip({
|
|||||||
open,
|
open,
|
||||||
side,
|
side,
|
||||||
sideOffset,
|
sideOffset,
|
||||||
delayDuration = 750,
|
|
||||||
}: {
|
}: {
|
||||||
className?: string;
|
className?: string;
|
||||||
style?: CSSProperties;
|
style?: CSSProperties;
|
||||||
@@ -28,11 +27,10 @@ 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={delayDuration} open={open}>
|
<ShadcnTooltip delayDuration={750} open={open}>
|
||||||
<TooltipTrigger asChild>{children}</TooltipTrigger>
|
<TooltipTrigger asChild>{children}</TooltipTrigger>
|
||||||
<TooltipContent
|
<TooltipContent
|
||||||
className={cn(className)}
|
className={cn(className)}
|
||||||
|
|||||||
@@ -78,12 +78,16 @@ 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: {
|
||||||
@@ -91,7 +95,9 @@ const starterKit = StarterKit.configure({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
listItem: {
|
listItem: {
|
||||||
HTMLAttributes: {},
|
HTMLAttributes: {
|
||||||
|
class: cx("leading-normal -mb-2"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
blockquote: {
|
blockquote: {
|
||||||
HTMLAttributes: {
|
HTMLAttributes: {
|
||||||
@@ -101,6 +107,7 @@ 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",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -377,14 +377,3 @@ 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,10 +4,6 @@
|
|||||||
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;
|
||||||
|
|||||||
Reference in New Issue
Block a user