mirror of
https://github.com/onyx-dot-app/onyx.git
synced 2026-02-25 11:45:47 +00:00
Compare commits
2 Commits
ci_python_
...
display_ci
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c59dd36d55 | ||
|
|
0558e687d9 |
@@ -12,7 +12,6 @@ from onyx.configs.app_configs import CODE_INTERPRETER_BASE_URL
|
||||
from onyx.configs.app_configs import CODE_INTERPRETER_DEFAULT_TIMEOUT_MS
|
||||
from onyx.configs.app_configs import CODE_INTERPRETER_MAX_OUTPUT_LENGTH
|
||||
from onyx.configs.constants import FileOrigin
|
||||
from onyx.db.code_interpreter import fetch_code_interpreter_server
|
||||
from onyx.file_store.utils import build_full_frontend_file_url
|
||||
from onyx.file_store.utils import get_default_file_store
|
||||
from onyx.server.query_and_chat.placement import Placement
|
||||
@@ -104,10 +103,8 @@ class PythonTool(Tool[PythonToolOverrideKwargs]):
|
||||
@override
|
||||
@classmethod
|
||||
def is_available(cls, db_session: Session) -> bool:
|
||||
if not CODE_INTERPRETER_BASE_URL:
|
||||
return False
|
||||
server = fetch_code_interpreter_server(db_session)
|
||||
return server.server_enabled
|
||||
is_available = bool(CODE_INTERPRETER_BASE_URL)
|
||||
return is_available
|
||||
|
||||
def tool_definition(self) -> dict:
|
||||
return {
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import requests
|
||||
|
||||
from tests.integration.common_utils.constants import API_SERVER_URL
|
||||
from tests.integration.common_utils.managers.tool import ToolManager
|
||||
from tests.integration.common_utils.test_models import DATestUser
|
||||
|
||||
CODE_INTERPRETER_URL = f"{API_SERVER_URL}/admin/code-interpreter"
|
||||
CODE_INTERPRETER_HEALTH_URL = f"{CODE_INTERPRETER_URL}/health"
|
||||
PYTHON_TOOL_NAME = "python"
|
||||
|
||||
|
||||
def test_get_code_interpreter_health_as_admin(
|
||||
@@ -96,35 +94,3 @@ def test_code_interpreter_endpoints_require_admin(
|
||||
headers=basic_user.headers,
|
||||
)
|
||||
assert put_response.status_code == 403
|
||||
|
||||
|
||||
def test_python_tool_hidden_from_tool_list_when_disabled(
|
||||
admin_user: DATestUser,
|
||||
) -> None:
|
||||
"""When code interpreter is disabled, the Python tool should not appear
|
||||
in the GET /tool response (i.e. the frontend tool list)."""
|
||||
# Disable
|
||||
response = requests.put(
|
||||
CODE_INTERPRETER_URL,
|
||||
json={"enabled": False},
|
||||
headers=admin_user.headers,
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
# Python tool should not be in the tool list
|
||||
tools = ToolManager.list_tools(user_performing_action=admin_user)
|
||||
tool_names = [t.name for t in tools]
|
||||
assert PYTHON_TOOL_NAME not in tool_names
|
||||
|
||||
# Re-enable
|
||||
response = requests.put(
|
||||
CODE_INTERPRETER_URL,
|
||||
json={"enabled": True},
|
||||
headers=admin_user.headers,
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
# Python tool should reappear
|
||||
tools = ToolManager.list_tools(user_performing_action=admin_user)
|
||||
tool_names = [t.name for t in tools]
|
||||
assert PYTHON_TOOL_NAME in tool_names
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
"""Tests for PythonTool availability based on server_enabled flag.
|
||||
|
||||
Verifies that PythonTool reports itself as unavailable when either:
|
||||
- CODE_INTERPRETER_BASE_URL is not set, or
|
||||
- CodeInterpreterServer.server_enabled is False in the database.
|
||||
"""
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
from unittest.mock import patch
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Unavailable when CODE_INTERPRETER_BASE_URL is not set
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
|
||||
@patch(
|
||||
"onyx.tools.tool_implementations.python.python_tool.CODE_INTERPRETER_BASE_URL",
|
||||
None,
|
||||
)
|
||||
def test_python_tool_unavailable_without_base_url() -> None:
|
||||
from onyx.tools.tool_implementations.python.python_tool import PythonTool
|
||||
|
||||
db_session = MagicMock(spec=Session)
|
||||
assert PythonTool.is_available(db_session) is False
|
||||
|
||||
|
||||
@patch(
|
||||
"onyx.tools.tool_implementations.python.python_tool.CODE_INTERPRETER_BASE_URL",
|
||||
"",
|
||||
)
|
||||
def test_python_tool_unavailable_with_empty_base_url() -> None:
|
||||
from onyx.tools.tool_implementations.python.python_tool import PythonTool
|
||||
|
||||
db_session = MagicMock(spec=Session)
|
||||
assert PythonTool.is_available(db_session) is False
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Unavailable when server_enabled is False
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
|
||||
@patch(
|
||||
"onyx.tools.tool_implementations.python.python_tool.CODE_INTERPRETER_BASE_URL",
|
||||
"http://localhost:8000",
|
||||
)
|
||||
@patch(
|
||||
"onyx.tools.tool_implementations.python.python_tool.fetch_code_interpreter_server",
|
||||
)
|
||||
def test_python_tool_unavailable_when_server_disabled(
|
||||
mock_fetch: MagicMock,
|
||||
) -> None:
|
||||
from onyx.tools.tool_implementations.python.python_tool import PythonTool
|
||||
|
||||
mock_server = MagicMock()
|
||||
mock_server.server_enabled = False
|
||||
mock_fetch.return_value = mock_server
|
||||
|
||||
db_session = MagicMock(spec=Session)
|
||||
assert PythonTool.is_available(db_session) is False
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Available when both conditions are met
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
|
||||
@patch(
|
||||
"onyx.tools.tool_implementations.python.python_tool.CODE_INTERPRETER_BASE_URL",
|
||||
"http://localhost:8000",
|
||||
)
|
||||
@patch(
|
||||
"onyx.tools.tool_implementations.python.python_tool.fetch_code_interpreter_server",
|
||||
)
|
||||
def test_python_tool_available_when_server_enabled(
|
||||
mock_fetch: MagicMock,
|
||||
) -> None:
|
||||
from onyx.tools.tool_implementations.python.python_tool import PythonTool
|
||||
|
||||
mock_server = MagicMock()
|
||||
mock_server.server_enabled = True
|
||||
mock_fetch.return_value = mock_server
|
||||
|
||||
db_session = MagicMock(spec=Session)
|
||||
assert PythonTool.is_available(db_session) is True
|
||||
@@ -1,8 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { MinimalPersonaSnapshot } from "@/app/admin/assistants/interfaces";
|
||||
import { useOnboardingState } from "@/refresh-components/onboarding/useOnboardingState";
|
||||
|
||||
function getOnboardingCompletedKey(userId: string): string {
|
||||
return `onyx:onboardingCompleted:${userId}`;
|
||||
}
|
||||
|
||||
interface UseShowOnboardingParams {
|
||||
liveAssistant: MinimalPersonaSnapshot | undefined;
|
||||
isLoadingProviders: boolean;
|
||||
@@ -21,6 +26,15 @@ export function useShowOnboarding({
|
||||
userId,
|
||||
}: UseShowOnboardingParams) {
|
||||
const [showOnboarding, setShowOnboarding] = useState(false);
|
||||
const [onboardingDismissed, setOnboardingDismissed] = useState(false);
|
||||
|
||||
// Read localStorage once userId is available to check if onboarding was dismissed
|
||||
useEffect(() => {
|
||||
if (userId === undefined) return;
|
||||
const dismissed =
|
||||
localStorage.getItem(getOnboardingCompletedKey(userId)) === "true";
|
||||
setOnboardingDismissed(dismissed);
|
||||
}, [userId]);
|
||||
|
||||
// Initialize onboarding state
|
||||
const {
|
||||
@@ -38,13 +52,23 @@ export function useShowOnboarding({
|
||||
// Show onboarding only if no LLM providers are configured.
|
||||
// Skip entirely if user has existing chat sessions.
|
||||
useEffect(() => {
|
||||
// If onboarding was previously dismissed, never show it again
|
||||
if (onboardingDismissed) {
|
||||
setShowOnboarding(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait for data to load
|
||||
if (isLoadingProviders || isLoadingChatSessions || userId === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only check once per user
|
||||
// Only check once per user — but allow self-correction from true→false
|
||||
// when provider data arrives (e.g. after a transient fetch error).
|
||||
if (hasCheckedOnboardingForUserId.current === userId) {
|
||||
if (showOnboarding && hasAnyProvider && onboardingState.stepIndex === 0) {
|
||||
setShowOnboarding(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
hasCheckedOnboardingForUserId.current = userId;
|
||||
@@ -63,18 +87,24 @@ export function useShowOnboarding({
|
||||
hasAnyProvider,
|
||||
chatSessionsCount,
|
||||
userId,
|
||||
showOnboarding,
|
||||
onboardingDismissed,
|
||||
onboardingState.stepIndex,
|
||||
]);
|
||||
|
||||
const hideOnboarding = () => {
|
||||
const dismissOnboarding = useCallback(() => {
|
||||
if (userId === undefined) return;
|
||||
setShowOnboarding(false);
|
||||
};
|
||||
setOnboardingDismissed(true);
|
||||
localStorage.setItem(getOnboardingCompletedKey(userId), "true");
|
||||
}, [userId]);
|
||||
|
||||
const finishOnboarding = () => {
|
||||
setShowOnboarding(false);
|
||||
};
|
||||
const hideOnboarding = dismissOnboarding;
|
||||
const finishOnboarding = dismissOnboarding;
|
||||
|
||||
return {
|
||||
showOnboarding,
|
||||
onboardingDismissed,
|
||||
onboardingState,
|
||||
onboardingActions,
|
||||
llmDescriptors,
|
||||
|
||||
@@ -7,6 +7,11 @@ interface LinguistLanguage {
|
||||
filenames?: string[];
|
||||
}
|
||||
|
||||
interface LanguageMaps {
|
||||
extensions: Map<string, string>;
|
||||
filenames: Map<string, string>;
|
||||
}
|
||||
|
||||
const allLanguages = Object.values(languages) as LinguistLanguage[];
|
||||
|
||||
// Collect extensions that linguist-languages assigns to "Markdown" so we can
|
||||
@@ -17,37 +22,58 @@ const markdownExtensions = new Set(
|
||||
?.extensions?.map((ext) => ext.toLowerCase()) ?? []
|
||||
);
|
||||
|
||||
// Build extension → language name and filename → language name maps at module load
|
||||
const extensionMap = new Map<string, string>();
|
||||
const filenameMap = new Map<string, string>();
|
||||
function buildLanguageMaps(
|
||||
type: string,
|
||||
excludedExtensions?: Set<string>
|
||||
): LanguageMaps {
|
||||
const extensions = new Map<string, string>();
|
||||
const filenames = new Map<string, string>();
|
||||
|
||||
for (const lang of allLanguages) {
|
||||
if (lang.type !== "programming") continue;
|
||||
for (const lang of allLanguages) {
|
||||
if (lang.type !== type) continue;
|
||||
|
||||
const name = lang.name.toLowerCase();
|
||||
for (const ext of lang.extensions ?? []) {
|
||||
if (markdownExtensions.has(ext.toLowerCase())) continue;
|
||||
// First language to claim an extension wins
|
||||
if (!extensionMap.has(ext)) {
|
||||
extensionMap.set(ext, name);
|
||||
}
|
||||
}
|
||||
for (const filename of lang.filenames ?? []) {
|
||||
if (!filenameMap.has(filename.toLowerCase())) {
|
||||
filenameMap.set(filename.toLowerCase(), name);
|
||||
const name = lang.name.toLowerCase();
|
||||
for (const ext of lang.extensions ?? []) {
|
||||
if (excludedExtensions?.has(ext.toLowerCase())) continue;
|
||||
if (!extensions.has(ext)) {
|
||||
extensions.set(ext, name);
|
||||
}
|
||||
}
|
||||
for (const filename of lang.filenames ?? []) {
|
||||
if (!filenames.has(filename.toLowerCase())) {
|
||||
filenames.set(filename.toLowerCase(), name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { extensions, filenames };
|
||||
}
|
||||
|
||||
function lookupLanguage(name: string, maps: LanguageMaps): string | null {
|
||||
const lower = name.toLowerCase();
|
||||
const ext = lower.match(/\.[^.]+$/)?.[0];
|
||||
return (ext && maps.extensions.get(ext)) ?? maps.filenames.get(lower) ?? null;
|
||||
}
|
||||
|
||||
const codeMaps = buildLanguageMaps("programming", markdownExtensions);
|
||||
const dataMaps = buildLanguageMaps("data");
|
||||
|
||||
/**
|
||||
* Returns the language name for a given file name, or null if it's not a
|
||||
* recognised code file. Looks up by extension first, then by exact filename
|
||||
* (e.g. "Dockerfile", "Makefile"). Runs in O(1).
|
||||
*/
|
||||
export function getCodeLanguage(name: string): string | null {
|
||||
const lower = name.toLowerCase();
|
||||
const ext = lower.match(/\.[^.]+$/)?.[0];
|
||||
return (ext && extensionMap.get(ext)) ?? filenameMap.get(lower) ?? null;
|
||||
return lookupLanguage(name, codeMaps);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the language name for a given file name if it's a recognised
|
||||
* "data" type in linguist-languages (e.g. JSON, YAML, TOML, XML).
|
||||
* Returns null otherwise. Runs in O(1).
|
||||
*/
|
||||
export function getDataLanguage(name: string): string | null {
|
||||
return lookupLanguage(name, dataMaps);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,11 +17,13 @@ const mockActions = {
|
||||
reset: jest.fn(),
|
||||
};
|
||||
|
||||
let mockStepIndex = 0;
|
||||
|
||||
jest.mock("@/refresh-components/onboarding/useOnboardingState", () => ({
|
||||
useOnboardingState: () => ({
|
||||
state: {
|
||||
currentStep: OnboardingStep.Welcome,
|
||||
stepIndex: 0,
|
||||
stepIndex: mockStepIndex,
|
||||
totalSteps: 3,
|
||||
data: {},
|
||||
isButtonActive: true,
|
||||
@@ -60,6 +62,8 @@ function renderUseShowOnboarding(
|
||||
describe("useShowOnboarding", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
localStorage.clear();
|
||||
mockStepIndex = 0;
|
||||
});
|
||||
|
||||
it("returns showOnboarding=false while providers are loading", () => {
|
||||
@@ -107,7 +111,7 @@ describe("useShowOnboarding", () => {
|
||||
expect(result.current.showOnboarding).toBe(false);
|
||||
});
|
||||
|
||||
it("only evaluates once per userId", () => {
|
||||
it("self-corrects showOnboarding to false when providers arrive late", () => {
|
||||
const { result, rerender } = renderUseShowOnboarding({
|
||||
hasAnyProvider: false,
|
||||
chatSessionsCount: 0,
|
||||
@@ -115,7 +119,7 @@ describe("useShowOnboarding", () => {
|
||||
});
|
||||
expect(result.current.showOnboarding).toBe(true);
|
||||
|
||||
// Re-render with same userId but different provider state
|
||||
// Re-render with same userId but provider data now available
|
||||
rerender({
|
||||
liveAssistant: undefined,
|
||||
isLoadingProviders: false,
|
||||
@@ -125,7 +129,32 @@ describe("useShowOnboarding", () => {
|
||||
userId: "user-1",
|
||||
});
|
||||
|
||||
// Should still be true because it was already evaluated for this userId
|
||||
// Should correct to false — providers exist, no need for LLM setup flow
|
||||
expect(result.current.showOnboarding).toBe(false);
|
||||
});
|
||||
|
||||
it("does not self-correct when user has advanced past Welcome step", () => {
|
||||
const { result, rerender } = renderUseShowOnboarding({
|
||||
hasAnyProvider: false,
|
||||
chatSessionsCount: 0,
|
||||
userId: "user-1",
|
||||
});
|
||||
expect(result.current.showOnboarding).toBe(true);
|
||||
|
||||
// Simulate user advancing past Welcome (e.g. they configured an LLM provider)
|
||||
mockStepIndex = 1;
|
||||
|
||||
// Re-render with same userId but provider data now available
|
||||
rerender({
|
||||
liveAssistant: undefined,
|
||||
isLoadingProviders: false,
|
||||
hasAnyProvider: true,
|
||||
isLoadingChatSessions: false,
|
||||
chatSessionsCount: 0,
|
||||
userId: "user-1",
|
||||
});
|
||||
|
||||
// Should stay true — user is actively using onboarding
|
||||
expect(result.current.showOnboarding).toBe(true);
|
||||
});
|
||||
|
||||
@@ -186,4 +215,83 @@ describe("useShowOnboarding", () => {
|
||||
expect(result.current.onboardingActions).toBeDefined();
|
||||
expect(result.current.llmDescriptors).toEqual([]);
|
||||
});
|
||||
|
||||
describe("localStorage persistence", () => {
|
||||
it("finishOnboarding sets localStorage flag and onboardingDismissed", () => {
|
||||
const { result } = renderUseShowOnboarding({
|
||||
hasAnyProvider: false,
|
||||
chatSessionsCount: 0,
|
||||
});
|
||||
expect(result.current.showOnboarding).toBe(true);
|
||||
expect(result.current.onboardingDismissed).toBe(false);
|
||||
|
||||
act(() => {
|
||||
result.current.finishOnboarding();
|
||||
});
|
||||
|
||||
expect(result.current.showOnboarding).toBe(false);
|
||||
expect(result.current.onboardingDismissed).toBe(true);
|
||||
expect(localStorage.getItem("onyx:onboardingCompleted:user-1")).toBe(
|
||||
"true"
|
||||
);
|
||||
});
|
||||
|
||||
it("hideOnboarding sets localStorage flag and onboardingDismissed", () => {
|
||||
const { result } = renderUseShowOnboarding({
|
||||
hasAnyProvider: false,
|
||||
chatSessionsCount: 0,
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.hideOnboarding();
|
||||
});
|
||||
|
||||
expect(result.current.onboardingDismissed).toBe(true);
|
||||
expect(localStorage.getItem("onyx:onboardingCompleted:user-1")).toBe(
|
||||
"true"
|
||||
);
|
||||
});
|
||||
|
||||
it("showOnboarding stays false when localStorage flag is set", () => {
|
||||
localStorage.setItem("onyx:onboardingCompleted:user-1", "true");
|
||||
|
||||
const { result } = renderUseShowOnboarding({
|
||||
hasAnyProvider: false,
|
||||
chatSessionsCount: 0,
|
||||
});
|
||||
|
||||
expect(result.current.showOnboarding).toBe(false);
|
||||
expect(result.current.onboardingDismissed).toBe(true);
|
||||
});
|
||||
|
||||
it("onboardingDismissed is false when localStorage flag is not set", () => {
|
||||
const { result } = renderUseShowOnboarding();
|
||||
expect(result.current.onboardingDismissed).toBe(false);
|
||||
});
|
||||
|
||||
it("dismissal for user-1 does not suppress onboarding for user-2", () => {
|
||||
const { result: result1 } = renderUseShowOnboarding({
|
||||
hasAnyProvider: false,
|
||||
chatSessionsCount: 0,
|
||||
userId: "1",
|
||||
});
|
||||
expect(result1.current.showOnboarding).toBe(true);
|
||||
|
||||
act(() => {
|
||||
result1.current.finishOnboarding();
|
||||
});
|
||||
expect(result1.current.onboardingDismissed).toBe(true);
|
||||
expect(localStorage.getItem("onyx:onboardingCompleted:1")).toBe("true");
|
||||
|
||||
// user-2 should still see onboarding
|
||||
const { result: result2 } = renderUseShowOnboarding({
|
||||
hasAnyProvider: false,
|
||||
chatSessionsCount: 0,
|
||||
userId: "2",
|
||||
});
|
||||
expect(result2.current.showOnboarding).toBe(true);
|
||||
expect(result2.current.onboardingDismissed).toBe(false);
|
||||
expect(localStorage.getItem("onyx:onboardingCompleted:2")).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -227,6 +227,7 @@ export default function AppPage({ firstMessage }: ChatPageProps) {
|
||||
|
||||
const {
|
||||
showOnboarding,
|
||||
onboardingDismissed,
|
||||
onboardingState,
|
||||
onboardingActions,
|
||||
llmDescriptors,
|
||||
@@ -462,7 +463,7 @@ export default function AppPage({ firstMessage }: ChatPageProps) {
|
||||
currentMessageFiles,
|
||||
deepResearch: deepResearchEnabled,
|
||||
});
|
||||
if (showOnboarding) {
|
||||
if (showOnboarding || !onboardingDismissed) {
|
||||
finishOnboarding();
|
||||
}
|
||||
},
|
||||
@@ -472,6 +473,7 @@ export default function AppPage({ firstMessage }: ChatPageProps) {
|
||||
currentMessageFiles,
|
||||
deepResearchEnabled,
|
||||
showOnboarding,
|
||||
onboardingDismissed,
|
||||
finishOnboarding,
|
||||
]
|
||||
);
|
||||
@@ -505,7 +507,7 @@ export default function AppPage({ firstMessage }: ChatPageProps) {
|
||||
currentMessageFiles,
|
||||
deepResearch: deepResearchEnabled,
|
||||
});
|
||||
if (showOnboarding) {
|
||||
if (showOnboarding || !onboardingDismissed) {
|
||||
finishOnboarding();
|
||||
}
|
||||
return;
|
||||
@@ -526,6 +528,7 @@ export default function AppPage({ firstMessage }: ChatPageProps) {
|
||||
currentMessageFiles,
|
||||
deepResearchEnabled,
|
||||
showOnboarding,
|
||||
onboardingDismissed,
|
||||
finishOnboarding,
|
||||
]
|
||||
);
|
||||
@@ -797,7 +800,8 @@ export default function AppPage({ firstMessage }: ChatPageProps) {
|
||||
{/* OnboardingUI */}
|
||||
{(appFocus.isNewSession() || appFocus.isAgent()) &&
|
||||
!classification &&
|
||||
(showOnboarding || !user?.personalization?.name) && (
|
||||
(showOnboarding || !user?.personalization?.name) &&
|
||||
!onboardingDismissed && (
|
||||
<OnboardingFlow
|
||||
showOnboarding={showOnboarding}
|
||||
handleHideOnboarding={hideOnboarding}
|
||||
|
||||
@@ -7,7 +7,7 @@ import Text from "@/refresh-components/texts/Text";
|
||||
import SimpleLoader from "@/refresh-components/loaders/SimpleLoader";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Section } from "@/layouts/general-layouts";
|
||||
import { getCodeLanguage } from "@/lib/languages";
|
||||
import { getCodeLanguage, getDataLanguage } from "@/lib/languages";
|
||||
import { fetchChatFile } from "@/lib/chat/svc";
|
||||
import { PreviewContext } from "@/sections/modals/PreviewModal/interfaces";
|
||||
import { resolveVariant } from "@/sections/modals/PreviewModal/variants";
|
||||
@@ -47,6 +47,7 @@ export default function PreviewModal({
|
||||
const language = useMemo(
|
||||
() =>
|
||||
getCodeLanguage(presentingDocument.semantic_identifier || "") ||
|
||||
getDataLanguage(presentingDocument.semantic_identifier || "") ||
|
||||
"plaintext",
|
||||
[presentingDocument.semantic_identifier]
|
||||
);
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import MinimalMarkdown from "@/components/chat/MinimalMarkdown";
|
||||
import Text from "@/refresh-components/texts/Text";
|
||||
import { Section } from "@/layouts/general-layouts";
|
||||
import { getDataLanguage } from "@/lib/languages";
|
||||
import { CodeBlock } from "@/app/app/message/CodeBlock";
|
||||
import { extractCodeText } from "@/app/app/message/codeUtils";
|
||||
import { PreviewVariant } from "@/sections/modals/PreviewModal/interfaces";
|
||||
import {
|
||||
CopyButton,
|
||||
DownloadButton,
|
||||
} from "@/sections/modals/PreviewModal/variants/shared";
|
||||
|
||||
function formatContent(language: string, content: string): string {
|
||||
if (language === "json") {
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(content), null, 2);
|
||||
} catch {
|
||||
return content;
|
||||
}
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
export const dataVariant: PreviewVariant = {
|
||||
matches: (name) => !!getDataLanguage(name || ""),
|
||||
width: "md",
|
||||
height: "lg",
|
||||
needsTextContent: true,
|
||||
|
||||
headerDescription: (ctx) =>
|
||||
ctx.fileContent
|
||||
? `${ctx.language} - ${ctx.lineCount} ${
|
||||
ctx.lineCount === 1 ? "line" : "lines"
|
||||
} · ${ctx.fileSize}`
|
||||
: "",
|
||||
|
||||
renderContent: (ctx) => {
|
||||
const formatted = formatContent(ctx.language, ctx.fileContent);
|
||||
return (
|
||||
<MinimalMarkdown
|
||||
content={`\`\`\`${ctx.language}\n${formatted}\n\n\`\`\``}
|
||||
className="w-full break-words h-full"
|
||||
components={{
|
||||
code: ({ node, children }: any) => {
|
||||
const codeText = extractCodeText(node, formatted, children);
|
||||
return (
|
||||
<CodeBlock className="" codeText={codeText}>
|
||||
{children}
|
||||
</CodeBlock>
|
||||
);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
||||
renderFooterLeft: (ctx) => (
|
||||
<Text text03 mainUiBody className="select-none">
|
||||
{ctx.lineCount} {ctx.lineCount === 1 ? "line" : "lines"}
|
||||
</Text>
|
||||
),
|
||||
|
||||
renderFooterRight: (ctx) => (
|
||||
<Section flexDirection="row" width="fit">
|
||||
<CopyButton getText={() => ctx.fileContent} />
|
||||
<DownloadButton fileUrl={ctx.fileUrl} fileName={ctx.fileName} />
|
||||
</Section>
|
||||
),
|
||||
};
|
||||
@@ -4,6 +4,7 @@ import { imageVariant } from "@/sections/modals/PreviewModal/variants/imageVaria
|
||||
import { pdfVariant } from "@/sections/modals/PreviewModal/variants/pdfVariant";
|
||||
import { csvVariant } from "@/sections/modals/PreviewModal/variants/csvVariant";
|
||||
import { markdownVariant } from "@/sections/modals/PreviewModal/variants/markdownVariant";
|
||||
import { dataVariant } from "@/sections/modals/PreviewModal/variants/dataVariant";
|
||||
import { unsupportedVariant } from "@/sections/modals/PreviewModal/variants/unsupportedVariant";
|
||||
|
||||
const PREVIEW_VARIANTS: PreviewVariant[] = [
|
||||
@@ -12,6 +13,7 @@ const PREVIEW_VARIANTS: PreviewVariant[] = [
|
||||
pdfVariant,
|
||||
csvVariant,
|
||||
markdownVariant,
|
||||
dataVariant,
|
||||
];
|
||||
|
||||
export function resolveVariant(
|
||||
|
||||
Reference in New Issue
Block a user