@tradejs/app 1.0.6 → 1.0.9

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.
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { useCallback, useEffect, useState } from 'react';
3
+ import { useEffect, useMemo, useState } from 'react';
4
4
  import {
5
5
  Button,
6
6
  CloseButton,
@@ -12,74 +12,42 @@ import {
12
12
  HStack,
13
13
  Stack,
14
14
  Text,
15
+ Alert,
15
16
  SkeletonCircle,
16
17
  SkeletonText,
17
18
  } from '@chakra-ui/react';
18
- import { AIChatMessage, AIChatHistory } from '@tradejs/types';
19
19
  import { GiArtificialHive } from 'react-icons/gi';
20
- import { useFilters } from '@store';
21
- import { sendMessage, getHistory } from '@actions/ai';
20
+ import { useAiChatStore, useFilters } from '@store';
22
21
  import { Message } from './Message';
23
22
 
24
23
  export const AiDrawer = () => {
25
24
  const [open, setOpen] = useState(false);
26
- const [loading, setLoading] = useState(true);
27
25
  const [input, setInput] = useState('');
28
- const [messages, setMessages] = useState<AIChatHistory>([]);
29
26
  const { filters } = useFilters();
30
-
31
- const loadHistory = useCallback(async () => {
32
- setLoading(true);
33
-
34
- const history = await getHistory(filters.symbol);
35
-
36
- setMessages(history);
37
-
38
- setLoading(false);
39
- }, [filters.symbol]);
27
+ const getChat = useAiChatStore((s) => s.getChat);
28
+ const loadHistory = useAiChatStore((s) => s.loadHistory);
29
+ const sendPrompt = useAiChatStore((s) => s.sendPrompt);
30
+ const sendQuickCommand = useAiChatStore((s) => s.sendQuickCommand);
31
+ const chat = getChat(filters.symbol);
32
+ const { loading, sending, error, messages } = chat;
33
+ const isBusy = loading || sending;
34
+ const canSend = useMemo(
35
+ () => input.trim().length > 0 && !sending,
36
+ [input, sending],
37
+ );
40
38
 
41
39
  useEffect(() => {
42
- void loadHistory();
43
- }, [loadHistory]);
40
+ void loadHistory(filters.symbol);
41
+ }, [filters.symbol, loadHistory]);
44
42
 
45
43
  const handleSend = async () => {
46
44
  if (!input.trim()) return;
47
-
48
- const message = {
49
- from: 'user',
50
- text: input,
51
- command: 'prompt',
52
- } as AIChatMessage;
53
-
54
- setMessages((state) => [...state, message]);
55
-
56
- const response = await sendMessage({ message, filters });
57
-
58
- setMessages((state) => [...state, response]);
59
-
45
+ await sendPrompt(filters, input);
60
46
  setInput('');
61
47
  };
62
48
 
63
49
  const handleQuick = async (command: string) => {
64
- let message: AIChatMessage | null = null;
65
-
66
- if (command === '/line') {
67
- message = {
68
- from: 'user',
69
- text: 'Какие наклонные линии можно построить на данном графике',
70
- command,
71
- };
72
- }
73
-
74
- if (!message) {
75
- return;
76
- }
77
-
78
- setMessages((state) => [...state, message as AIChatMessage]);
79
-
80
- const response = await sendMessage({ message, filters });
81
-
82
- setMessages((state) => [...state, response]);
50
+ await sendQuickCommand(filters, command);
83
51
  };
84
52
 
85
53
  return (
@@ -106,6 +74,15 @@ export const AiDrawer = () => {
106
74
  </Drawer.Header>
107
75
 
108
76
  <Drawer.Body overflowY="auto" flex="1">
77
+ {error ? (
78
+ <Alert.Root status="error" mb={4}>
79
+ <Alert.Indicator />
80
+ <Alert.Content>
81
+ <Alert.Title>AI chat error</Alert.Title>
82
+ <Alert.Description>{error}</Alert.Description>
83
+ </Alert.Content>
84
+ </Alert.Root>
85
+ ) : null}
109
86
  {loading ? (
110
87
  <Stack gap="4" maxW="xs">
111
88
  <HStack width="full">
@@ -128,6 +105,7 @@ export const AiDrawer = () => {
128
105
  <Button
129
106
  size="sm"
130
107
  variant="outline"
108
+ disabled={isBusy}
131
109
  onClick={() => handleQuick('/line')}
132
110
  >
133
111
  /line
@@ -135,6 +113,7 @@ export const AiDrawer = () => {
135
113
  <Button
136
114
  size="sm"
137
115
  variant="outline"
116
+ disabled={isBusy}
138
117
  onClick={() => handleQuick('/analyze')}
139
118
  >
140
119
  /analyze
@@ -148,10 +127,18 @@ export const AiDrawer = () => {
148
127
  rows={3}
149
128
  maxH="15lh"
150
129
  value={input}
130
+ disabled={sending}
151
131
  onChange={(e) => setInput(e.target.value)}
152
132
  />
153
133
 
154
- <Button mt={2} size={'sm'} variant="subtle" onClick={handleSend}>
134
+ <Button
135
+ mt={2}
136
+ size={'sm'}
137
+ variant="subtle"
138
+ disabled={!canSend}
139
+ loading={sending}
140
+ onClick={handleSend}
141
+ >
155
142
  Send
156
143
  </Button>
157
144
  </Drawer.Footer>
@@ -6,7 +6,8 @@ import { Select } from '@UI';
6
6
  import { useFiltersContext } from '../context';
7
7
 
8
8
  export const SelectBacktest = () => {
9
- const { filters, backtestFiles, onChangeFilters } = useFiltersContext();
9
+ const { filters, backtestFiles, onChangeFilters, ensureBacktestsLoaded } =
10
+ useFiltersContext();
10
11
  const STORAGE_KEY = 'backtest-strategy';
11
12
 
12
13
  const tests = useMemo(
@@ -127,10 +128,6 @@ export const SelectBacktest = () => {
127
128
  }
128
129
  };
129
130
 
130
- if (_.isEmpty(tests) || _.isEmpty(strategyItems) || !selectedStrategy) {
131
- return null;
132
- }
133
-
134
131
  const strategyTests = tests.filter(
135
132
  (test) => test.data?.strategyName === selectedStrategy,
136
133
  );
@@ -142,6 +139,11 @@ export const SelectBacktest = () => {
142
139
  defaultValue={[selectedStrategy]}
143
140
  value={[selectedStrategy]}
144
141
  onChange={onChangeStrategy}
142
+ onOpenChange={(open) => {
143
+ if (open) {
144
+ void ensureBacktestsLoaded?.();
145
+ }
146
+ }}
145
147
  items={strategyItems}
146
148
  width="220px"
147
149
  />
@@ -150,6 +152,11 @@ export const SelectBacktest = () => {
150
152
  defaultValue={[filters.backtestId || '']}
151
153
  value={[filters.backtestId || '']}
152
154
  onChange={onChange}
155
+ onOpenChange={(open) => {
156
+ if (open) {
157
+ void ensureBacktestsLoaded?.();
158
+ }
159
+ }}
153
160
  items={[
154
161
  {
155
162
  label: 'Not selected',
@@ -9,6 +9,8 @@ interface RootProps {
9
9
  backtestFiles: Items;
10
10
  filters: UIFilters;
11
11
  onChangeFilters?: OnChangeFilters;
12
+ ensureTickersLoaded?: () => void | Promise<unknown>;
13
+ ensureBacktestsLoaded?: () => void | Promise<unknown>;
12
14
  }
13
15
 
14
16
  export const Root = ({
@@ -16,11 +18,20 @@ export const Root = ({
16
18
  tickers,
17
19
  backtestFiles,
18
20
  onChangeFilters,
21
+ ensureTickersLoaded,
22
+ ensureBacktestsLoaded,
19
23
  children,
20
24
  }: PropsWithChildren<RootProps>) => {
21
25
  return (
22
26
  <FiltersContext.Provider
23
- value={{ filters, tickers, backtestFiles, onChangeFilters }}
27
+ value={{
28
+ filters,
29
+ tickers,
30
+ backtestFiles,
31
+ onChangeFilters,
32
+ ensureTickersLoaded,
33
+ ensureBacktestsLoaded,
34
+ }}
24
35
  >
25
36
  {children}
26
37
  </FiltersContext.Provider>
@@ -2,14 +2,13 @@
2
2
 
3
3
  import _ from 'lodash';
4
4
  import { SelectWithSearch } from '@UI';
5
- import { SkeletonText, Stack, Show } from '@chakra-ui/react';
6
5
  import { useFiltersContext } from '../context';
7
6
 
8
7
  interface SelectSymbolProps {}
9
8
 
10
9
  export const SelectSymbol = ({}: SelectSymbolProps) => {
11
- const { filters, tickers, onChangeFilters } = useFiltersContext();
12
- const loading = _.isEmpty(tickers);
10
+ const { filters, tickers, onChangeFilters, ensureTickersLoaded } =
11
+ useFiltersContext();
13
12
  const defaultInputValue =
14
13
  tickers.find(({ value }) => value === filters.symbol)?.label ||
15
14
  filters.symbol;
@@ -29,21 +28,17 @@ export const SelectSymbol = ({}: SelectSymbolProps) => {
29
28
  };
30
29
 
31
30
  return (
32
- <Show
33
- when={!loading}
34
- fallback={
35
- <Stack width="240px">
36
- <SkeletonText height={8} noOfLines={1} />
37
- </Stack>
38
- }
39
- >
40
- <SelectWithSearch
41
- defaultValue={[filters.symbol]}
42
- defaultInputValue={defaultInputValue}
43
- onChange={onChange}
44
- items={tickers}
45
- width="240px"
46
- />
47
- </Show>
31
+ <SelectWithSearch
32
+ defaultValue={[filters.symbol]}
33
+ defaultInputValue={defaultInputValue}
34
+ onChange={onChange}
35
+ onOpenChange={(open) => {
36
+ if (open) {
37
+ void ensureTickersLoaded?.();
38
+ }
39
+ }}
40
+ items={tickers}
41
+ width="240px"
42
+ />
48
43
  );
49
44
  };
@@ -6,6 +6,8 @@ interface FiltersContextProps {
6
6
  tickers: Items;
7
7
  backtestFiles: Items;
8
8
  onChangeFilters?: OnChangeFilters;
9
+ ensureTickersLoaded?: () => void | Promise<unknown>;
10
+ ensureBacktestsLoaded?: () => void | Promise<unknown>;
9
11
  }
10
12
 
11
13
  export const FiltersContext = createContext<FiltersContextProps>(