🧑💻 feat: Add telegram api debugging capabilities
This commit is contained in:
parent
2454da15ff
commit
23c5758e0f
9 changed files with 394 additions and 42 deletions
|
|
@ -24,6 +24,7 @@ import io.vertx.ext.web.handler.CorsHandler;
|
||||||
import io.vertx.ext.web.handler.SessionHandler;
|
import io.vertx.ext.web.handler.SessionHandler;
|
||||||
import io.vertx.ext.web.sstore.LocalSessionStore;
|
import io.vertx.ext.web.sstore.LocalSessionStore;
|
||||||
import io.vertx.ext.web.sstore.SessionStore;
|
import io.vertx.ext.web.sstore.SessionStore;
|
||||||
|
import org.drinkless.tdlib.TdApi;
|
||||||
import telegram.files.repository.SettingKey;
|
import telegram.files.repository.SettingKey;
|
||||||
import telegram.files.repository.SettingRecord;
|
import telegram.files.repository.SettingRecord;
|
||||||
import telegram.files.repository.TelegramRecord;
|
import telegram.files.repository.TelegramRecord;
|
||||||
|
|
@ -130,6 +131,8 @@ public class HttpVerticle extends AbstractVerticle {
|
||||||
|
|
||||||
router.post("/telegram/create").handler(this::handleTelegramCreate);
|
router.post("/telegram/create").handler(this::handleTelegramCreate);
|
||||||
router.post("/telegram/:telegramId/delete").handler(this::handleTelegramDelete);
|
router.post("/telegram/:telegramId/delete").handler(this::handleTelegramDelete);
|
||||||
|
router.get("/telegram/api/methods").handler(this::handleTelegramApiMethods);
|
||||||
|
router.get("/telegram/api/:method/parameters").handler(this::handleTelegramApiMethodParameters);
|
||||||
router.post("/telegram/api/:method").handler(this::handleTelegramApi);
|
router.post("/telegram/api/:method").handler(this::handleTelegramApi);
|
||||||
router.get("/telegrams").handler(this::handleTelegrams);
|
router.get("/telegrams").handler(this::handleTelegrams);
|
||||||
router.get("/telegram/:telegramId/chats").handler(this::handleTelegramChats);
|
router.get("/telegram/:telegramId/chats").handler(this::handleTelegramChats);
|
||||||
|
|
@ -415,7 +418,7 @@ public class HttpVerticle extends AbstractVerticle {
|
||||||
|
|
||||||
String type = ctx.request().getParam("type");
|
String type = ctx.request().getParam("type");
|
||||||
String timeRange = ctx.request().getParam("timeRange");
|
String timeRange = ctx.request().getParam("timeRange");
|
||||||
(Objects.equals(type, "phase") ? telegramVerticle. getDownloadStatisticsByPhase(Convert.toInt(timeRange, 1)) :
|
(Objects.equals(type, "phase") ? telegramVerticle.getDownloadStatisticsByPhase(Convert.toInt(timeRange, 1)) :
|
||||||
telegramVerticle.getDownloadStatistics())
|
telegramVerticle.getDownloadStatistics())
|
||||||
.onSuccess(ctx::json)
|
.onSuccess(ctx::json)
|
||||||
.onFailure(ctx::fail);
|
.onFailure(ctx::fail);
|
||||||
|
|
@ -459,6 +462,16 @@ public class HttpVerticle extends AbstractVerticle {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void handleTelegramApiMethods(RoutingContext ctx) {
|
||||||
|
Map<String, Class<TdApi.Function<?>>> functions = TdApiHelp.getFunctions();
|
||||||
|
ctx.json(JsonObject.of("methods", functions.keySet()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleTelegramApiMethodParameters(RoutingContext ctx) {
|
||||||
|
String method = ctx.pathParam("method");
|
||||||
|
ctx.json(JsonObject.of("parameters", TdApiHelp.getFunction(method, null)));
|
||||||
|
}
|
||||||
|
|
||||||
private void handleTelegramApi(RoutingContext ctx) {
|
private void handleTelegramApi(RoutingContext ctx) {
|
||||||
String method = ctx.pathParam("method");
|
String method = ctx.pathParam("method");
|
||||||
if (method == null) {
|
if (method == null) {
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,10 @@ public class TdApiHelp {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Map<String, Class<TdApi.Function<?>>> getFunctions() {
|
||||||
|
return FUNCTIONS;
|
||||||
|
}
|
||||||
|
|
||||||
public static TdApi.Function<?> getFunction(String method, Object params) {
|
public static TdApi.Function<?> getFunction(String method, Object params) {
|
||||||
Class<TdApi.Function<?>> func = FUNCTIONS.get(method);
|
Class<TdApi.Function<?>> func = FUNCTIONS.get(method);
|
||||||
if (func == null) {
|
if (func == null) {
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,14 @@ Cookie: {{tf}}
|
||||||
GET http://{{server}}/telegram/{{telegramId}}/download-statistics?type=phase&timeRange=3
|
GET http://{{server}}/telegram/{{telegramId}}/download-statistics?type=phase&timeRange=3
|
||||||
Cookie: {{tf}}
|
Cookie: {{tf}}
|
||||||
|
|
||||||
|
### Telegram api methods
|
||||||
|
GET http://{{server}}/telegram/api/methods
|
||||||
|
Cookie: {{tf}}
|
||||||
|
|
||||||
|
### Telegram api method parameters
|
||||||
|
GET http://{{server}}/telegram/api/GetChats/parameters
|
||||||
|
Cookie: {{tf}}
|
||||||
|
|
||||||
### Telegram GetMe
|
### Telegram GetMe
|
||||||
POST http://{{server}}/telegram/api/GetMe
|
POST http://{{server}}/telegram/api/GetMe
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|
|
||||||
300
web/src/components/debug-telegram-method.tsx
Normal file
300
web/src/components/debug-telegram-method.tsx
Normal file
|
|
@ -0,0 +1,300 @@
|
||||||
|
import { useTelegramMethod } from "@/hooks/use-telegram-method";
|
||||||
|
import useSWR from "swr";
|
||||||
|
import { request } from "@/lib/api";
|
||||||
|
import { useMemo, useState } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import {
|
||||||
|
Command,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandInput,
|
||||||
|
CommandItem,
|
||||||
|
CommandList,
|
||||||
|
CommandSeparator,
|
||||||
|
} from "@/components/ui/command";
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "@/components/ui/popover";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import {
|
||||||
|
Check,
|
||||||
|
ChevronsUpDown,
|
||||||
|
CircleAlert,
|
||||||
|
Copy,
|
||||||
|
Ellipsis,
|
||||||
|
Loader2,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { CommandLoading } from "cmdk";
|
||||||
|
import { useDebounce } from "use-debounce";
|
||||||
|
import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
interface TelegramMethodData {
|
||||||
|
methods: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TelegramMethodParameters {
|
||||||
|
parameters: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DebugTelegramMethod() {
|
||||||
|
const { triggerMethod, isMethodExecuting, lastMethodCode, lastMethodResult } =
|
||||||
|
useTelegramMethod();
|
||||||
|
const [isDeMethodExecuting] = useDebounce(isMethodExecuting, 500, {
|
||||||
|
leading: true,
|
||||||
|
});
|
||||||
|
const [, copyToClipboard] = useCopyToClipboard();
|
||||||
|
const [method, setMethod] = useState<string>("");
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
|
const [parameters, setParameters] = useState<any>({});
|
||||||
|
const [parametersJson, setParametersJson] = useState<string>("");
|
||||||
|
const [error, setError] = useState<string>("");
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
|
||||||
|
const { data: methodData, isLoading: isMethodsLoading } = useSWR<
|
||||||
|
TelegramMethodData,
|
||||||
|
Error
|
||||||
|
>("/telegram/api/methods", request, {
|
||||||
|
// 添加缓存以提升性能
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
revalidateOnReconnect: false,
|
||||||
|
dedupingInterval: 60000,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 使用 useMemo 缓存过滤后的方法列表
|
||||||
|
const filteredMethods = useMemo(() => {
|
||||||
|
if (!methodData?.methods || isMethodsLoading) return [];
|
||||||
|
|
||||||
|
return methodData.methods
|
||||||
|
.filter((m) => m.toLowerCase().includes(searchQuery.toLowerCase()))
|
||||||
|
.slice(0, 100)
|
||||||
|
.sort();
|
||||||
|
}, [isMethodsLoading, methodData?.methods, searchQuery]);
|
||||||
|
|
||||||
|
const methodDocUrl = useMemo(() => {
|
||||||
|
if (!method) return "";
|
||||||
|
const methodSlug = method.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase();
|
||||||
|
return `https://core.telegram.org/tdlib/docs/classtd_1_1td__api_1_1${methodSlug}.html`;
|
||||||
|
}, [method]);
|
||||||
|
|
||||||
|
const { isLoading: isParametersLoading } = useSWR<
|
||||||
|
TelegramMethodParameters,
|
||||||
|
Error
|
||||||
|
>(method ? `/telegram/api/${method}/parameters` : null, request, {
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
onSuccess: (data) => {
|
||||||
|
const defaultParams = data.parameters;
|
||||||
|
setParameters(defaultParams);
|
||||||
|
setParametersJson(JSON.stringify(defaultParams, null, 2));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleMethodChange = (value: string) => {
|
||||||
|
setMethod(value);
|
||||||
|
setError("");
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleParametersChange = (value: string) => {
|
||||||
|
setParametersJson(value);
|
||||||
|
try {
|
||||||
|
setParameters(JSON.parse(value));
|
||||||
|
setError("");
|
||||||
|
} catch (e) {
|
||||||
|
setError("Invalid JSON format");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleExecute = async () => {
|
||||||
|
try {
|
||||||
|
await triggerMethod({
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
|
data: parameters,
|
||||||
|
method,
|
||||||
|
});
|
||||||
|
setError("");
|
||||||
|
} catch (e) {
|
||||||
|
setError(e instanceof Error ? e.message : "An error occurred");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isLoading = isMethodsLoading || isParametersLoading;
|
||||||
|
const isExecuteDisabled = !method || !!error || isMethodExecuting;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="mx-auto h-full w-full overflow-y-scroll">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Telegram API Debug</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-3">
|
||||||
|
{/* Method Selection */}
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<label className="text-sm font-medium">Method</label>
|
||||||
|
{isMethodsLoading && (
|
||||||
|
<div className="flex items-center text-sm text-muted-foreground">
|
||||||
|
<Loader2 className="mr-2 h-3 w-3 animate-spin" />
|
||||||
|
Loading methods...
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Popover open={open} onOpenChange={setOpen}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
role="combobox"
|
||||||
|
aria-expanded={open}
|
||||||
|
className="w-full justify-between"
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
{method ? method : "Select a method..."}
|
||||||
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-full p-0" modal={true} align="start">
|
||||||
|
<Command shouldFilter={false}>
|
||||||
|
<CommandInput
|
||||||
|
placeholder="Search method..."
|
||||||
|
value={searchQuery}
|
||||||
|
onValueChange={setSearchQuery}
|
||||||
|
/>
|
||||||
|
<CommandList className="relative">
|
||||||
|
{isMethodsLoading && (
|
||||||
|
<CommandLoading>
|
||||||
|
<div className="absolute left-1/2 top-1/2 -translate-x-2 -translate-y-2 transform">
|
||||||
|
<Ellipsis className="h-4 w-4 animate-pulse" />
|
||||||
|
</div>
|
||||||
|
</CommandLoading>
|
||||||
|
)}
|
||||||
|
{!isMethodsLoading && filteredMethods.length === 0 && (
|
||||||
|
<CommandEmpty>No methods found.</CommandEmpty>
|
||||||
|
)}
|
||||||
|
{!isMethodExecuting && searchQuery.length === 0 && (
|
||||||
|
<>
|
||||||
|
<CommandGroup>
|
||||||
|
<CommandItem disabled={true}>
|
||||||
|
<CircleAlert className="h-3 w-3" />
|
||||||
|
Only the first 100 methods are shown.
|
||||||
|
</CommandItem>
|
||||||
|
</CommandGroup>
|
||||||
|
<CommandSeparator />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<CommandGroup>
|
||||||
|
{filteredMethods.map((m) => (
|
||||||
|
<CommandItem
|
||||||
|
key={m}
|
||||||
|
value={m}
|
||||||
|
onSelect={handleMethodChange}
|
||||||
|
>
|
||||||
|
<Check
|
||||||
|
className={cn(
|
||||||
|
"h-4 w-4",
|
||||||
|
method === m ? "opacity-100" : "opacity-0",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{m}
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
{method && (
|
||||||
|
<Link
|
||||||
|
href={methodDocUrl}
|
||||||
|
target="_blank"
|
||||||
|
className="text-xs text-blue-500 underline"
|
||||||
|
>
|
||||||
|
{method} Documentation
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Parameters Editor */}
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<label className="text-sm font-medium">Parameters</label>
|
||||||
|
{isParametersLoading && (
|
||||||
|
<div className="flex items-center text-sm text-muted-foreground">
|
||||||
|
<Loader2 className="mr-2 h-3 w-3 animate-spin" />
|
||||||
|
Loading parameters...
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Textarea
|
||||||
|
value={parametersJson}
|
||||||
|
onChange={(e) => handleParametersChange(e.target.value)}
|
||||||
|
placeholder="Enter parameters in JSON format"
|
||||||
|
className="h-48 font-mono text-sm"
|
||||||
|
disabled={!method || isLoading}
|
||||||
|
/>
|
||||||
|
{error && <p className="mt-1 text-sm text-red-500">{error}</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Execute Button */}
|
||||||
|
<Button
|
||||||
|
onClick={handleExecute}
|
||||||
|
disabled={isExecuteDisabled}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{isDeMethodExecuting ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
Executing...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
"Execute Method"
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* Result Display */}
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<label className="text-sm font-medium">
|
||||||
|
Result
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{lastMethodCode ? `(${lastMethodCode})` : ""}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
{isMethodExecuting && (
|
||||||
|
<div className="flex items-center text-sm text-muted-foreground">
|
||||||
|
<Loader2 className="mr-2 h-3 w-3 animate-spin" />
|
||||||
|
Processing...
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="group relative">
|
||||||
|
<Textarea
|
||||||
|
value={
|
||||||
|
lastMethodResult
|
||||||
|
? JSON.stringify(lastMethodResult, null, 2)
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
readOnly
|
||||||
|
placeholder="Method execution result will appear here"
|
||||||
|
className="h-48 font-mono text-sm"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
if (!lastMethodResult) return;
|
||||||
|
copyToClipboard(JSON.stringify(lastMethodResult, null, 2));
|
||||||
|
}}
|
||||||
|
className="absolute right-2 top-2 opacity-0 group-hover:opacity-100"
|
||||||
|
>
|
||||||
|
<Copy className="h-2 w-2" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -33,30 +33,6 @@ export default function FileProgress({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-end justify-between gap-2">
|
<div className="flex items-end justify-between gap-2">
|
||||||
{/*<div className="hidden min-w-20 md:flex">*/}
|
|
||||||
{/* <div className="group flex items-center gap-1 px-1">*/}
|
|
||||||
{/* <AnimatePresence mode="wait">*/}
|
|
||||||
{/* <motion.div*/}
|
|
||||||
{/* key="content"*/}
|
|
||||||
{/* className="flex items-center gap-1"*/}
|
|
||||||
{/* initial={{ opacity: 1 }}*/}
|
|
||||||
{/* exit={{ opacity: 0 }}*/}
|
|
||||||
{/* >*/}
|
|
||||||
{/* <Zap className="h-3 w-3 group-hover:hidden" />*/}
|
|
||||||
{/* <ClockArrowDown className="hidden h-3 w-3 group-hover:block" />*/}
|
|
||||||
{/* <span className="text-nowrap text-xs">*/}
|
|
||||||
{/* <span className="group-hover:hidden">*/}
|
|
||||||
{/* {`${prettyBytes(downloadSpeed, { bits: true })}/s`}*/}
|
|
||||||
{/* </span>*/}
|
|
||||||
{/* <span className="hidden group-hover:inline">*/}
|
|
||||||
{/* {prettyBytes(file.downloadedSize)}*/}
|
|
||||||
{/* </span>*/}
|
|
||||||
{/* </span>*/}
|
|
||||||
{/* </motion.div>*/}
|
|
||||||
{/* </AnimatePresence>*/}
|
|
||||||
{/* </div>*/}
|
|
||||||
{/*</div>*/}
|
|
||||||
|
|
||||||
<Progress value={progress} className="flex-1 rounded-none md:w-32" />
|
<Progress value={progress} className="flex-1 rounded-none md:w-32" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import About from "@/components/about";
|
||||||
import { ChartColumnIncreasingIcon } from "@/components/ui/chart-column-increasing";
|
import { ChartColumnIncreasingIcon } from "@/components/ui/chart-column-increasing";
|
||||||
import { LayoutPanelTopIcon } from "@/components/ui/layout-panel-top";
|
import { LayoutPanelTopIcon } from "@/components/ui/layout-panel-top";
|
||||||
import FilePhaseStatistics from "@/components/file-phase-statistics";
|
import FilePhaseStatistics from "@/components/file-phase-statistics";
|
||||||
|
import DebugTelegramMethod from "@/components/debug-telegram-method";
|
||||||
|
|
||||||
export const SettingsDialog: React.FC = () => {
|
export const SettingsDialog: React.FC = () => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
@ -50,6 +51,7 @@ export const SettingsDialog: React.FC = () => {
|
||||||
<TabsTrigger value="general">General</TabsTrigger>
|
<TabsTrigger value="general">General</TabsTrigger>
|
||||||
<TabsTrigger value="statistics">Statistics</TabsTrigger>
|
<TabsTrigger value="statistics">Statistics</TabsTrigger>
|
||||||
<TabsTrigger value="proxys">Proxys</TabsTrigger>
|
<TabsTrigger value="proxys">Proxys</TabsTrigger>
|
||||||
|
<TabsTrigger value="api">API</TabsTrigger>
|
||||||
<TabsTrigger value="about">About</TabsTrigger>
|
<TabsTrigger value="about">About</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
<TabsContent value="general" className="overflow-hidden">
|
<TabsContent value="general" className="overflow-hidden">
|
||||||
|
|
@ -60,10 +62,16 @@ export const SettingsDialog: React.FC = () => {
|
||||||
{accountId ? (
|
{accountId ? (
|
||||||
<Tabs defaultValue="panel">
|
<Tabs defaultValue="panel">
|
||||||
<TabsList className="bg-none!">
|
<TabsList className="bg-none!">
|
||||||
<TabsTrigger value="panel" className="w-10 h-10 scale-75 data-[state=active]:bg-accent">
|
<TabsTrigger
|
||||||
|
value="panel"
|
||||||
|
className="h-10 w-10 scale-75 data-[state=active]:bg-accent"
|
||||||
|
>
|
||||||
<LayoutPanelTopIcon />
|
<LayoutPanelTopIcon />
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="phase" className="w-10 h-10 scale-75 data-[state=active]:bg-accent">
|
<TabsTrigger
|
||||||
|
value="phase"
|
||||||
|
className="h-10 w-10 scale-75 data-[state=active]:bg-accent"
|
||||||
|
>
|
||||||
<ChartColumnIncreasingIcon />
|
<ChartColumnIncreasingIcon />
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
@ -92,6 +100,9 @@ export const SettingsDialog: React.FC = () => {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
<TabsContent value="api" className="h-full overflow-hidden">
|
||||||
|
<DebugTelegramMethod />
|
||||||
|
</TabsContent>
|
||||||
<TabsContent value="about" className="h-full overflow-hidden">
|
<TabsContent value="about" className="h-full overflow-hidden">
|
||||||
<About />
|
<About />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
|
||||||
|
|
@ -13,21 +13,33 @@ const PopoverAnchor = PopoverPrimitive.Anchor;
|
||||||
|
|
||||||
const PopoverContent = React.forwardRef<
|
const PopoverContent = React.forwardRef<
|
||||||
React.ElementRef<typeof PopoverPrimitive.Content>,
|
React.ElementRef<typeof PopoverPrimitive.Content>,
|
||||||
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> & {
|
||||||
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
modal?: boolean;
|
||||||
<PopoverPrimitive.Portal>
|
}
|
||||||
<PopoverPrimitive.Content
|
>(
|
||||||
ref={ref}
|
(
|
||||||
align={align}
|
{ className, align = "center", sideOffset = 4, modal = false, ...props },
|
||||||
sideOffset={sideOffset}
|
ref,
|
||||||
className={cn(
|
) => {
|
||||||
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
const content = (
|
||||||
className,
|
<PopoverPrimitive.Content
|
||||||
)}
|
ref={ref}
|
||||||
{...props}
|
align={align}
|
||||||
/>
|
sideOffset={sideOffset}
|
||||||
</PopoverPrimitive.Portal>
|
className={cn(
|
||||||
));
|
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
return !modal ? (
|
||||||
|
<PopoverPrimitive.Portal>{content}</PopoverPrimitive.Portal>
|
||||||
|
) : (
|
||||||
|
content
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
|
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
|
||||||
|
|
||||||
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };
|
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };
|
||||||
|
|
|
||||||
22
web/src/components/ui/textarea.tsx
Normal file
22
web/src/components/ui/textarea.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const Textarea = React.forwardRef<
|
||||||
|
HTMLTextAreaElement,
|
||||||
|
React.ComponentProps<"textarea">
|
||||||
|
>(({ className, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<textarea
|
||||||
|
className={cn(
|
||||||
|
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
Textarea.displayName = "Textarea";
|
||||||
|
|
||||||
|
export { Textarea };
|
||||||
|
|
@ -5,8 +5,10 @@ import { useEffect, useMemo, useState } from "react";
|
||||||
import { useWebsocket } from "@/hooks/use-websocket";
|
import { useWebsocket } from "@/hooks/use-websocket";
|
||||||
|
|
||||||
export function useTelegramMethod() {
|
export function useTelegramMethod() {
|
||||||
|
const [lastMethodCode, setLastMethodCode] = useState<string>();
|
||||||
const [methodCodes, setMethodCodes] = useState<string[]>([]);
|
const [methodCodes, setMethodCodes] = useState<string[]>([]);
|
||||||
const [methodCompleteCodes, setMethodCompleteCodes] = useState<string[]>([]);
|
const [methodCompleteCodes, setMethodCompleteCodes] = useState<string[]>([]);
|
||||||
|
const [lastMethodResult, setLastMethodResult] = useState<unknown>();
|
||||||
const { lastJsonMessage } = useWebsocket();
|
const { lastJsonMessage } = useWebsocket();
|
||||||
|
|
||||||
const { trigger: triggerMethod, isMutating: isMethodMutating } =
|
const { trigger: triggerMethod, isMutating: isMethodMutating } =
|
||||||
|
|
@ -15,6 +17,7 @@ export function useTelegramMethod() {
|
||||||
telegramApi,
|
telegramApi,
|
||||||
{
|
{
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
|
setLastMethodCode(data.code);
|
||||||
setMethodCodes((prev) => [...prev, data.code]);
|
setMethodCodes((prev) => [...prev, data.code]);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -24,6 +27,7 @@ export function useTelegramMethod() {
|
||||||
if (!lastJsonMessage) return;
|
if (!lastJsonMessage) return;
|
||||||
if (lastJsonMessage.code) {
|
if (lastJsonMessage.code) {
|
||||||
setMethodCompleteCodes((prev) => [...prev, lastJsonMessage.code]);
|
setMethodCompleteCodes((prev) => [...prev, lastJsonMessage.code]);
|
||||||
|
setLastMethodResult(lastJsonMessage.data)
|
||||||
}
|
}
|
||||||
}, [lastJsonMessage]);
|
}, [lastJsonMessage]);
|
||||||
|
|
||||||
|
|
@ -37,5 +41,7 @@ export function useTelegramMethod() {
|
||||||
return {
|
return {
|
||||||
triggerMethod,
|
triggerMethod,
|
||||||
isMethodExecuting,
|
isMethodExecuting,
|
||||||
|
lastMethodCode,
|
||||||
|
lastMethodResult,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue