@tradejs/app 1.0.5 → 1.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/tradejs-app.mjs +129 -20
- package/package.json +13 -12
- package/public/auth-bg.jpg +0 -0
- package/public/next.svg +1 -0
- package/public/og-image-source.svg +91 -0
- package/public/og-image.png +0 -0
- package/public/vercel.svg +1 -0
- package/src/app/api/ai/route.ts +84 -20
- package/src/app/api/backtest/files/route.ts +12 -1
- package/src/app/api/backtest/test/[strategy]/[name]/route.ts +18 -1
- package/src/app/api/kline/[provider]/[symbol]/[interval]/route.ts +357 -29
- package/src/app/api/scanner/[provider]/route.ts +7 -1
- package/src/app/api/scanner/route.ts +7 -1
- package/src/app/api/signal/[symbol]/[signalId]/route.ts +6 -0
- package/src/app/api/user/settings/route.ts +244 -0
- package/src/app/components/Dashboard/AiDrawer/index.tsx +38 -51
- package/src/app/components/Shared/Filters/Backtest/index.tsx +12 -5
- package/src/app/components/Shared/Filters/Root/index.tsx +12 -1
- package/src/app/components/Shared/Filters/Symbol/index.tsx +14 -19
- package/src/app/components/Shared/Filters/context.ts +2 -0
- package/src/app/components/Shared/Sidebar/AccountSettingsDrawer.tsx +948 -0
- package/src/app/components/Shared/Sidebar/index.tsx +13 -9
- package/src/app/components/UI/ColorMode/index.tsx +62 -15
- package/src/app/components/UI/Select/index.tsx +3 -0
- package/src/app/components/UI/SelectWithSearch/index.tsx +3 -0
- package/src/app/globals.css +11 -0
- package/src/app/layout.tsx +50 -11
- package/src/app/lib/currentUser.ts +27 -0
- package/src/app/lib/klineWindow.ts +17 -0
- package/src/app/routes/dashboard/[provider]/[symbol]/[interval]/page.tsx +10 -2
- package/src/app/routes/signin/page.tsx +11 -2
- package/src/app/store/ai.ts +174 -0
- package/src/app/store/data.ts +219 -88
- package/src/app/store/index.ts +1 -0
- package/src/app/store/tests.ts +96 -9
- package/src/app/store/tickers.ts +113 -17
- package/src/proxy.ts +23 -50
|
@@ -4,6 +4,7 @@ import { Box, Flex, IconButton, VStack } from '@chakra-ui/react';
|
|
|
4
4
|
import { useRouter, usePathname } from 'next/navigation';
|
|
5
5
|
import { signOut } from 'next-auth/react';
|
|
6
6
|
import { FiActivity, FiBarChart2, FiLogOut, FiPlay } from 'react-icons/fi';
|
|
7
|
+
import { AccountSettingsDrawer } from './AccountSettingsDrawer';
|
|
7
8
|
|
|
8
9
|
export const Sidebar = () => {
|
|
9
10
|
const router = useRouter();
|
|
@@ -57,15 +58,18 @@ export const Sidebar = () => {
|
|
|
57
58
|
))}
|
|
58
59
|
</VStack>
|
|
59
60
|
|
|
60
|
-
<
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
61
|
+
<VStack gap={2}>
|
|
62
|
+
<AccountSettingsDrawer />
|
|
63
|
+
<IconButton
|
|
64
|
+
aria-label="Sign out"
|
|
65
|
+
size="md"
|
|
66
|
+
colorPalette="teal"
|
|
67
|
+
variant="outline"
|
|
68
|
+
onClick={() => signOut({ callbackUrl: '/routes/signin' })}
|
|
69
|
+
>
|
|
70
|
+
<FiLogOut />
|
|
71
|
+
</IconButton>
|
|
72
|
+
</VStack>
|
|
69
73
|
</Flex>
|
|
70
74
|
</Box>
|
|
71
75
|
);
|
|
@@ -8,18 +8,9 @@ import {
|
|
|
8
8
|
type IconButtonProps,
|
|
9
9
|
type SpanProps,
|
|
10
10
|
} from '@chakra-ui/react';
|
|
11
|
-
import { ThemeProvider, useTheme, type ThemeProviderProps } from 'next-themes';
|
|
12
11
|
import * as React from 'react';
|
|
13
12
|
import { LuMoon, LuSun } from 'react-icons/lu';
|
|
14
13
|
|
|
15
|
-
export interface ColorModeProviderProps extends ThemeProviderProps {}
|
|
16
|
-
|
|
17
|
-
export function ColorModeProvider(props: ColorModeProviderProps) {
|
|
18
|
-
return (
|
|
19
|
-
<ThemeProvider attribute="class" disableTransitionOnChange {...props} />
|
|
20
|
-
);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
14
|
export type ColorMode = 'light' | 'dark';
|
|
24
15
|
|
|
25
16
|
export interface UseColorModeReturn {
|
|
@@ -28,16 +19,72 @@ export interface UseColorModeReturn {
|
|
|
28
19
|
toggleColorMode: () => void;
|
|
29
20
|
}
|
|
30
21
|
|
|
31
|
-
export
|
|
32
|
-
|
|
22
|
+
export interface ColorModeProviderProps {
|
|
23
|
+
children: React.ReactNode;
|
|
24
|
+
forcedTheme?: ColorMode;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const ColorModeContext = React.createContext<UseColorModeReturn | null>(null);
|
|
28
|
+
|
|
29
|
+
export function ColorModeProvider({
|
|
30
|
+
children,
|
|
31
|
+
forcedTheme,
|
|
32
|
+
}: ColorModeProviderProps) {
|
|
33
|
+
const [colorMode, setColorModeState] = React.useState<ColorMode>(
|
|
34
|
+
forcedTheme ?? 'dark',
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
React.useEffect(() => {
|
|
38
|
+
if (!forcedTheme) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
setColorModeState(forcedTheme);
|
|
43
|
+
}, [forcedTheme]);
|
|
44
|
+
|
|
45
|
+
React.useEffect(() => {
|
|
46
|
+
const root = document.documentElement;
|
|
47
|
+
const nextColorMode = forcedTheme ?? colorMode;
|
|
48
|
+
const previousColorMode = nextColorMode === 'dark' ? 'light' : 'dark';
|
|
49
|
+
|
|
50
|
+
root.classList.remove(previousColorMode);
|
|
51
|
+
root.classList.add(nextColorMode);
|
|
52
|
+
root.style.colorScheme = nextColorMode;
|
|
53
|
+
}, [colorMode, forcedTheme]);
|
|
54
|
+
|
|
55
|
+
const setColorMode = (nextColorMode: ColorMode) => {
|
|
56
|
+
if (forcedTheme) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
setColorModeState(nextColorMode);
|
|
61
|
+
};
|
|
62
|
+
|
|
33
63
|
const toggleColorMode = () => {
|
|
34
|
-
|
|
64
|
+
setColorMode(colorMode === 'dark' ? 'light' : 'dark');
|
|
35
65
|
};
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
66
|
+
|
|
67
|
+
const value: UseColorModeReturn = {
|
|
68
|
+
colorMode: forcedTheme ?? colorMode,
|
|
69
|
+
setColorMode,
|
|
39
70
|
toggleColorMode,
|
|
40
71
|
};
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<ColorModeContext.Provider value={value}>
|
|
75
|
+
{children}
|
|
76
|
+
</ColorModeContext.Provider>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function useColorMode(): UseColorModeReturn {
|
|
81
|
+
return (
|
|
82
|
+
React.useContext(ColorModeContext) ?? {
|
|
83
|
+
colorMode: 'dark',
|
|
84
|
+
setColorMode: () => {},
|
|
85
|
+
toggleColorMode: () => {},
|
|
86
|
+
}
|
|
87
|
+
);
|
|
41
88
|
}
|
|
42
89
|
|
|
43
90
|
export function useColorModeValue<T>(light: T, dark: T) {
|
|
@@ -19,6 +19,7 @@ interface SelectProps {
|
|
|
19
19
|
multiple?: boolean;
|
|
20
20
|
size?: 'xs' | 'sm' | 'md' | 'lg';
|
|
21
21
|
onChange?: (value: string[]) => void;
|
|
22
|
+
onOpenChange?: (open: boolean) => void;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
export const Select = ({
|
|
@@ -30,6 +31,7 @@ export const Select = ({
|
|
|
30
31
|
width = '320px',
|
|
31
32
|
size = 'sm',
|
|
32
33
|
onChange,
|
|
34
|
+
onOpenChange,
|
|
33
35
|
}: SelectProps) => {
|
|
34
36
|
const collection = useMemo(
|
|
35
37
|
() =>
|
|
@@ -44,6 +46,7 @@ export const Select = ({
|
|
|
44
46
|
collection={collection}
|
|
45
47
|
{...(value ? { value } : { defaultValue })}
|
|
46
48
|
onValueChange={(details) => onChange?.(details.value)}
|
|
49
|
+
onOpenChange={(details) => onOpenChange?.(details.open)}
|
|
47
50
|
size={size}
|
|
48
51
|
multiple={multiple}
|
|
49
52
|
width={width}
|
|
@@ -23,6 +23,7 @@ interface SelectWithSearchProps {
|
|
|
23
23
|
multiple?: boolean;
|
|
24
24
|
size?: 'xs' | 'sm' | 'md' | 'lg';
|
|
25
25
|
onChange?: (value: string[]) => void;
|
|
26
|
+
onOpenChange?: (open: boolean) => void;
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
export const SelectWithSearch = ({
|
|
@@ -35,6 +36,7 @@ export const SelectWithSearch = ({
|
|
|
35
36
|
width = '320px',
|
|
36
37
|
size = 'sm',
|
|
37
38
|
onChange,
|
|
39
|
+
onOpenChange,
|
|
38
40
|
}: SelectWithSearchProps) => {
|
|
39
41
|
const { contains } = useFilter({ sensitivity: 'base' });
|
|
40
42
|
const [inputValue, setInputValue] = useState(
|
|
@@ -61,6 +63,7 @@ export const SelectWithSearch = ({
|
|
|
61
63
|
setInputValue(e.inputValue);
|
|
62
64
|
}}
|
|
63
65
|
onOpenChange={(details) => {
|
|
66
|
+
onOpenChange?.(details.open);
|
|
64
67
|
if (details.open) {
|
|
65
68
|
filter('');
|
|
66
69
|
setInputValue('');
|
package/src/app/globals.css
CHANGED
package/src/app/layout.tsx
CHANGED
|
@@ -1,15 +1,51 @@
|
|
|
1
1
|
import type { Metadata } from 'next';
|
|
2
|
-
import { Inter } from 'next/font/google';
|
|
3
|
-
import { ClientOnly } from '@chakra-ui/react';
|
|
4
2
|
import { AppShell } from '@shared/AppShell';
|
|
5
3
|
import Provider from './provider';
|
|
6
4
|
import './globals.css';
|
|
7
5
|
|
|
8
|
-
const
|
|
6
|
+
const fallbackMetadataBase = 'http://localhost:3000';
|
|
7
|
+
|
|
8
|
+
const metadataBase = (() => {
|
|
9
|
+
const rawAppUrl = String(process.env.APP_URL || '').trim();
|
|
10
|
+
|
|
11
|
+
if (!rawAppUrl) {
|
|
12
|
+
return new URL(fallbackMetadataBase);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
return new URL(rawAppUrl);
|
|
17
|
+
} catch {
|
|
18
|
+
return new URL(fallbackMetadataBase);
|
|
19
|
+
}
|
|
20
|
+
})();
|
|
9
21
|
|
|
10
22
|
export const metadata: Metadata = {
|
|
23
|
+
metadataBase,
|
|
11
24
|
title: 'TradeJS App',
|
|
12
|
-
description:
|
|
25
|
+
description:
|
|
26
|
+
'TradeJS app for dashboards, backtests, charts, derivatives, and runtime data.',
|
|
27
|
+
applicationName: 'TradeJS App',
|
|
28
|
+
openGraph: {
|
|
29
|
+
title: 'TradeJS App',
|
|
30
|
+
description:
|
|
31
|
+
'TradeJS app for dashboards, backtests, charts, derivatives, and runtime data.',
|
|
32
|
+
type: 'website',
|
|
33
|
+
images: [
|
|
34
|
+
{
|
|
35
|
+
url: '/og-image.png',
|
|
36
|
+
width: 1200,
|
|
37
|
+
height: 630,
|
|
38
|
+
alt: 'TradeJS App',
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
twitter: {
|
|
43
|
+
card: 'summary_large_image',
|
|
44
|
+
title: 'TradeJS App',
|
|
45
|
+
description:
|
|
46
|
+
'Dashboards, backtests, charts, derivatives, and runtime data in one UI.',
|
|
47
|
+
images: ['/og-image.png'],
|
|
48
|
+
},
|
|
13
49
|
};
|
|
14
50
|
|
|
15
51
|
export default function RootLayout({
|
|
@@ -18,13 +54,16 @@ export default function RootLayout({
|
|
|
18
54
|
children: React.ReactNode;
|
|
19
55
|
}>) {
|
|
20
56
|
return (
|
|
21
|
-
<html
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
57
|
+
<html
|
|
58
|
+
lang="en"
|
|
59
|
+
className="dark"
|
|
60
|
+
style={{ colorScheme: 'dark' }}
|
|
61
|
+
suppressHydrationWarning
|
|
62
|
+
>
|
|
63
|
+
<body suppressHydrationWarning>
|
|
64
|
+
<Provider>
|
|
65
|
+
<AppShell>{children}</AppShell>
|
|
66
|
+
</Provider>
|
|
28
67
|
</body>
|
|
29
68
|
</html>
|
|
30
69
|
);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { auth } from '@app/auth';
|
|
2
|
+
|
|
3
|
+
type SessionLike = {
|
|
4
|
+
user?: {
|
|
5
|
+
id?: string;
|
|
6
|
+
name?: string | null;
|
|
7
|
+
};
|
|
8
|
+
} | null;
|
|
9
|
+
|
|
10
|
+
const readSessionUserName = (session: SessionLike) => {
|
|
11
|
+
const fromId = session?.user?.id;
|
|
12
|
+
if (typeof fromId === 'string' && fromId.trim()) {
|
|
13
|
+
return fromId.trim();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const fromName = session?.user?.name;
|
|
17
|
+
if (typeof fromName === 'string' && fromName.trim()) {
|
|
18
|
+
return fromName.trim();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return null;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const getCurrentUserName = async (): Promise<string | null> => {
|
|
25
|
+
const session = (await auth()) as SessionLike;
|
|
26
|
+
return readSessionUserName(session);
|
|
27
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { intervalToMs } from '@tradejs/core/data';
|
|
2
|
+
import { Interval } from '@tradejs/types';
|
|
3
|
+
|
|
4
|
+
export const normalizeEndToIntervalBoundary = (
|
|
5
|
+
end: number,
|
|
6
|
+
interval: Interval,
|
|
7
|
+
): number => {
|
|
8
|
+
const stepMs = intervalToMs(interval);
|
|
9
|
+
if (!Number.isFinite(end) || stepMs <= 0) {
|
|
10
|
+
return end;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return Math.floor(end / stepMs) * stepMs;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const getCurrentIntervalBoundary = (interval: Interval) =>
|
|
17
|
+
normalizeEndToIntervalBoundary(Date.now(), interval);
|
|
@@ -11,8 +11,14 @@ import { Interval, OnChangeFilters, Provider } from '@tradejs/types';
|
|
|
11
11
|
const Dashboard = () => {
|
|
12
12
|
const searchParams = useSearchParams();
|
|
13
13
|
const { filters, setFilters } = useFilters();
|
|
14
|
-
const { tickers } = useTickers(
|
|
15
|
-
|
|
14
|
+
const { tickers, ensureLoaded: ensureTickersLoaded } = useTickers(
|
|
15
|
+
filters.provider || 'bybit',
|
|
16
|
+
{ enabled: false },
|
|
17
|
+
);
|
|
18
|
+
const { tests, ensureLoaded: ensureBacktestsLoaded } = useTestList({
|
|
19
|
+
symbol: filters.symbol,
|
|
20
|
+
enabled: false,
|
|
21
|
+
});
|
|
16
22
|
const hasBacktestId = searchParams.has('backtestId');
|
|
17
23
|
const hasBacktestStrategy = searchParams.has('backtestStrategy');
|
|
18
24
|
const backtestId = searchParams.get('backtestId');
|
|
@@ -103,6 +109,8 @@ const Dashboard = () => {
|
|
|
103
109
|
tickers={tickers}
|
|
104
110
|
backtestFiles={tests}
|
|
105
111
|
onChangeFilters={onChangeFilters}
|
|
112
|
+
ensureTickersLoaded={ensureTickersLoaded}
|
|
113
|
+
ensureBacktestsLoaded={ensureBacktestsLoaded}
|
|
106
114
|
>
|
|
107
115
|
<Flex mb={2} gap={4} alignItems="center" flexDirection="row">
|
|
108
116
|
<Filters.SelectProvider />
|
|
@@ -75,8 +75,17 @@ const SigninContent = () => {
|
|
|
75
75
|
<Text fontSize="sm" opacity={0.7} letterSpacing="0.2em">
|
|
76
76
|
SIGN IN
|
|
77
77
|
</Text>
|
|
78
|
-
<Text
|
|
79
|
-
|
|
78
|
+
<Text
|
|
79
|
+
fontSize="2xl"
|
|
80
|
+
fontWeight="700"
|
|
81
|
+
letterSpacing="-0.03em"
|
|
82
|
+
lineHeight="1"
|
|
83
|
+
color="white"
|
|
84
|
+
>
|
|
85
|
+
<Box as="span">Trade</Box>
|
|
86
|
+
<Box as="span" color="#20c5bd">
|
|
87
|
+
JS
|
|
88
|
+
</Box>
|
|
80
89
|
</Text>
|
|
81
90
|
</Stack>
|
|
82
91
|
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
import { getHistory, sendMessage } from '@actions/ai';
|
|
3
|
+
import { AIChatHistory, AIChatMessage, Filters } from '@tradejs/types';
|
|
4
|
+
|
|
5
|
+
type AiChatEntry = {
|
|
6
|
+
loading: boolean;
|
|
7
|
+
sending: boolean;
|
|
8
|
+
loaded: boolean;
|
|
9
|
+
error: string | null;
|
|
10
|
+
messages: AIChatHistory;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
interface AiChatState {
|
|
14
|
+
chats: Record<string, AiChatEntry>;
|
|
15
|
+
getChat: (symbol: string) => AiChatEntry;
|
|
16
|
+
loadHistory: (symbol: string) => Promise<void>;
|
|
17
|
+
sendPrompt: (filters: Filters, input: string) => Promise<void>;
|
|
18
|
+
sendQuickCommand: (filters: Filters, command: string) => Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const EMPTY_CHAT: AiChatEntry = {
|
|
22
|
+
loading: false,
|
|
23
|
+
sending: false,
|
|
24
|
+
loaded: false,
|
|
25
|
+
error: null,
|
|
26
|
+
messages: [],
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const normalizeSymbolKey = (symbol: string) => symbol.trim().toUpperCase();
|
|
30
|
+
|
|
31
|
+
const getQuickMessage = (command: string): AIChatMessage | null => {
|
|
32
|
+
if (command === '/line') {
|
|
33
|
+
return {
|
|
34
|
+
from: 'user',
|
|
35
|
+
text: 'Какие наклонные линии можно построить на данном графике',
|
|
36
|
+
command,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return null;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const updateChat = (
|
|
44
|
+
chats: Record<string, AiChatEntry>,
|
|
45
|
+
symbol: string,
|
|
46
|
+
patch: Partial<AiChatEntry>,
|
|
47
|
+
) => ({
|
|
48
|
+
...chats,
|
|
49
|
+
[symbol]: {
|
|
50
|
+
...(chats[symbol] ?? EMPTY_CHAT),
|
|
51
|
+
...patch,
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
export const useAiChatStore = create<AiChatState>((set, get) => ({
|
|
56
|
+
chats: {},
|
|
57
|
+
|
|
58
|
+
getChat: (symbol) => get().chats[normalizeSymbolKey(symbol)] ?? EMPTY_CHAT,
|
|
59
|
+
|
|
60
|
+
loadHistory: async (symbol) => {
|
|
61
|
+
const symbolKey = normalizeSymbolKey(symbol);
|
|
62
|
+
if (!symbolKey) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const existing = get().chats[symbolKey];
|
|
67
|
+
if (existing?.loading) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
set((state) => ({
|
|
72
|
+
chats: updateChat(state.chats, symbolKey, {
|
|
73
|
+
loading: true,
|
|
74
|
+
error: null,
|
|
75
|
+
}),
|
|
76
|
+
}));
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const history = await getHistory(symbol);
|
|
80
|
+
set((state) => ({
|
|
81
|
+
chats: updateChat(state.chats, symbolKey, {
|
|
82
|
+
loading: false,
|
|
83
|
+
loaded: true,
|
|
84
|
+
messages: history,
|
|
85
|
+
}),
|
|
86
|
+
}));
|
|
87
|
+
} catch (error) {
|
|
88
|
+
set((state) => ({
|
|
89
|
+
chats: updateChat(state.chats, symbolKey, {
|
|
90
|
+
loading: false,
|
|
91
|
+
error: error instanceof Error ? error.message : 'Failed to load chat',
|
|
92
|
+
}),
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
sendPrompt: async (filters, input) => {
|
|
98
|
+
const trimmed = input.trim();
|
|
99
|
+
if (!trimmed) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const message: AIChatMessage = {
|
|
104
|
+
from: 'user',
|
|
105
|
+
text: trimmed,
|
|
106
|
+
command: 'prompt',
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const symbolKey = normalizeSymbolKey(filters.symbol);
|
|
110
|
+
|
|
111
|
+
set((state) => ({
|
|
112
|
+
chats: updateChat(state.chats, symbolKey, {
|
|
113
|
+
sending: true,
|
|
114
|
+
error: null,
|
|
115
|
+
loaded: true,
|
|
116
|
+
messages: [...(state.chats[symbolKey]?.messages ?? []), message],
|
|
117
|
+
}),
|
|
118
|
+
}));
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const response = await sendMessage({ message, filters });
|
|
122
|
+
set((state) => ({
|
|
123
|
+
chats: updateChat(state.chats, symbolKey, {
|
|
124
|
+
sending: false,
|
|
125
|
+
messages: [...(state.chats[symbolKey]?.messages ?? []), response],
|
|
126
|
+
}),
|
|
127
|
+
}));
|
|
128
|
+
} catch (error) {
|
|
129
|
+
set((state) => ({
|
|
130
|
+
chats: updateChat(state.chats, symbolKey, {
|
|
131
|
+
sending: false,
|
|
132
|
+
error:
|
|
133
|
+
error instanceof Error ? error.message : 'Failed to send message',
|
|
134
|
+
}),
|
|
135
|
+
}));
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
sendQuickCommand: async (filters, command) => {
|
|
140
|
+
const message = getQuickMessage(command);
|
|
141
|
+
if (!message) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const symbolKey = normalizeSymbolKey(filters.symbol);
|
|
146
|
+
|
|
147
|
+
set((state) => ({
|
|
148
|
+
chats: updateChat(state.chats, symbolKey, {
|
|
149
|
+
sending: true,
|
|
150
|
+
error: null,
|
|
151
|
+
loaded: true,
|
|
152
|
+
messages: [...(state.chats[symbolKey]?.messages ?? []), message],
|
|
153
|
+
}),
|
|
154
|
+
}));
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
const response = await sendMessage({ message, filters });
|
|
158
|
+
set((state) => ({
|
|
159
|
+
chats: updateChat(state.chats, symbolKey, {
|
|
160
|
+
sending: false,
|
|
161
|
+
messages: [...(state.chats[symbolKey]?.messages ?? []), response],
|
|
162
|
+
}),
|
|
163
|
+
}));
|
|
164
|
+
} catch (error) {
|
|
165
|
+
set((state) => ({
|
|
166
|
+
chats: updateChat(state.chats, symbolKey, {
|
|
167
|
+
sending: false,
|
|
168
|
+
error:
|
|
169
|
+
error instanceof Error ? error.message : 'Failed to send message',
|
|
170
|
+
}),
|
|
171
|
+
}));
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
}));
|