Add read-only production runtime audit

This commit is contained in:
2026-08-17 11:17:40 +07:00
parent 057d4ed9dc
commit a1de4715a4
106 changed files with 6869 additions and 1782 deletions
+79 -7
View File
@@ -1,7 +1,8 @@
"use client";
import React, { useState, useEffect, useRef } from "react";
import { Send, Square, Sparkles, Pill, Search, Command } from "lucide-react";
import type { MonographPickerState } from "@duoc-thu/shared-types";
import { Send, Square, Sparkles, Pill, Search, Command, X, BookOpen } from "lucide-react";
import { cn } from "@duoc-thu/ui";
interface ComposerProps {
@@ -9,6 +10,12 @@ interface ComposerProps {
isLoading?: boolean;
onStop?: () => void;
initialValue?: string;
monographPicker?: MonographPickerState | null;
onToggleSection?: (sectionKey: string) => void;
onClearMonograph?: () => void;
onSubmitMonograph?: () => void;
responseMode?: "ai" | "monograph";
onResponseModeChange?: (mode: "ai" | "monograph") => void;
className?: string;
}
@@ -17,6 +24,12 @@ export function Composer({
isLoading = false,
onStop,
initialValue = "",
monographPicker,
onToggleSection,
onClearMonograph,
onSubmitMonograph,
responseMode = "ai",
onResponseModeChange,
className,
}: ComposerProps) {
const [value, setValue] = useState(initialValue);
@@ -103,7 +116,12 @@ export function Composer({
const handleSubmit = () => {
const trimmed = value.trim();
if (!trimmed || isLoading) return;
if (isLoading) return;
if (!trimmed && monographPicker) {
onSubmitMonograph?.();
return;
}
if (!trimmed) return;
onSubmit(trimmed);
setValue("");
setSuggestions([]);
@@ -181,6 +199,36 @@ export function Composer({
{/* Main Composer Box */}
<div className="relative flex flex-col rounded-3xl border border-border-subtle bg-surface p-2 shadow-surface transition-all focus-within:border-border-accent focus-within:shadow-elevated glass-panel">
{monographPicker && (
<div className="flex flex-wrap items-center gap-2 px-2 pt-1 pb-2 border-b border-border-subtle/50">
<button
type="button"
onClick={onClearMonograph}
className="inline-flex items-center gap-1.5 rounded-full bg-accent-primary px-3 py-1.5 text-xs font-bold text-txt-inverse"
>
<Pill className="h-3.5 w-3.5" />
{monographPicker.drugName}
<X className="h-3.5 w-3.5" />
</button>
{monographPicker.selectedSectionKeys.map((sectionKey) => {
const section = monographPicker.sections.find(
(item) => item.sectionKey === sectionKey
);
if (!section) return null;
return (
<button
key={sectionKey}
type="button"
onClick={() => onToggleSection?.(sectionKey)}
className="inline-flex items-center gap-1.5 rounded-full border border-border-accent bg-accent-soft px-3 py-1.5 text-xs font-semibold text-accent-primary"
>
{section.sectionTitle}
<X className="h-3.5 w-3.5" />
</button>
);
})}
</div>
)}
<textarea
ref={inputRef}
value={value}
@@ -192,9 +240,33 @@ export function Composer({
/>
<div className="flex items-center justify-between gap-2 pt-2 px-2 border-t border-border-subtle/50">
<div className="flex items-center gap-1.5 text-[0.7rem] text-txt-muted">
<Command className="w-3 h-3" />
<span className="hidden sm:inline">Nhấn Enter đ gửi Shift+Enter đ xuống dòng</span>
<div className="flex items-center gap-2">
<button
type="button"
onClick={() =>
onResponseModeChange?.(
responseMode === "ai" ? "monograph" : "ai"
)
}
className={cn(
"inline-flex items-center gap-1.5 rounded-xl border px-2.5 py-1.5 text-[0.7rem] font-bold transition-colors",
responseMode === "monograph"
? "border-border-accent bg-accent-soft text-accent-primary"
: "border-border-subtle bg-surface-elevated text-txt-secondary hover:text-accent-primary"
)}
title="Chuyển giữa AI tổng hợp và tra nguyên văn chuyên luận"
>
{responseMode === "ai" ? (
<Sparkles className="h-3.5 w-3.5" />
) : (
<BookOpen className="h-3.5 w-3.5" />
)}
{responseMode === "ai" ? "AI tổng hợp" : "Chuyên luận"}
</button>
<div className="hidden items-center gap-1.5 text-[0.7rem] text-txt-muted sm:flex">
<Command className="w-3 h-3" />
<span>Enter đ gửi Shift+Enter đ xuống dòng</span>
</div>
</div>
<div className="flex items-center gap-2">
@@ -210,11 +282,11 @@ export function Composer({
) : (
<button
onClick={handleSubmit}
disabled={!value.trim()}
disabled={!value.trim() && !monographPicker}
type="button"
className={cn(
"flex items-center gap-1.5 px-4 py-1.5 rounded-xl text-xs font-bold transition-all shadow-sm",
value.trim()
value.trim() || monographPicker
? "bg-accent-primary text-txt-inverse hover:bg-accent-hover"
: "bg-surface-elevated text-txt-muted cursor-not-allowed"
)}