@minnai/create-aura-app 0.0.10 → 0.0.12

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.
Files changed (33) hide show
  1. package/dist/scaffold.js +8 -32
  2. package/package.json +1 -1
  3. package/templates/starter/package.json +3 -0
  4. package/templates/starter/src/src/App.css +32 -0
  5. package/templates/starter/src/src/App.tsx +82 -0
  6. package/templates/starter/src/src/ambiance/currency-air/index.tsx +25 -0
  7. package/templates/starter/src/src/ambiance/currency-air/logic.ts +49 -0
  8. package/templates/starter/src/src/ambiance/currency-air/manifest.ts +15 -0
  9. package/templates/starter/src/src/ambiance/currency-air/resources.ts +16 -0
  10. package/templates/starter/src/src/ambiance/currency-air/ui/index.tsx +42 -0
  11. package/templates/starter/src/src/ambiance/index.ts +48 -0
  12. package/templates/starter/src/src/ambiance/stocks-air/index.ts +3 -0
  13. package/templates/starter/src/src/ambiance/stocks-air/index.tsx +28 -0
  14. package/templates/starter/src/src/ambiance/stocks-air/logic.ts +87 -0
  15. package/templates/starter/src/src/ambiance/stocks-air/manifest.ts +15 -0
  16. package/templates/starter/src/src/ambiance/stocks-air/resources.ts +23 -0
  17. package/templates/starter/src/src/ambiance/stocks-air/ui/index.tsx +67 -0
  18. package/templates/starter/src/src/assets/react.svg +1 -0
  19. package/templates/starter/src/src/components/AnalyticsTracker.tsx +13 -0
  20. package/templates/starter/src/src/components/Playground/CodeEditor.tsx +121 -0
  21. package/templates/starter/src/src/components/Playground/Debugger.tsx +71 -0
  22. package/templates/starter/src/src/components/Playground/Playground.tsx +221 -0
  23. package/templates/starter/src/src/components/Playground/Sidebar.tsx +68 -0
  24. package/templates/starter/src/src/components/ProjectSidebar/ProjectSidebar.tsx +219 -0
  25. package/templates/starter/src/src/components/TourGuide/TourGuide.tsx +16 -0
  26. package/templates/starter/src/src/components/TourGuide/index.ts +1 -0
  27. package/templates/starter/src/src/components/TourGuide/tour-flow.yaml +137 -0
  28. package/templates/starter/src/src/components/TourGuide/useTourEngine.ts +376 -0
  29. package/templates/starter/src/src/index.css +68 -0
  30. package/templates/starter/src/src/main.tsx +10 -0
  31. package/templates/starter/src/src/services/AnalyticsService.ts +181 -0
  32. package/templates/starter/src/src/types/ContextHandler.ts +13 -0
  33. package/templates/starter/vite.config.ts +0 -9
package/dist/scaffold.js CHANGED
@@ -11,26 +11,15 @@ const colors_1 = require("kleur/colors");
11
11
  async function scaffold(options) {
12
12
  const { root, template, install, git, projectName } = options;
13
13
  console.log((0, colors_1.dim)(`Scaffolding project in ${root}...`));
14
- console.log((0, colors_1.dim)(`Current working directory: ${process.cwd()}`));
15
- console.log((0, colors_1.dim)(`PATH: ${process.env.PATH?.substring(0, 100)}...`));
16
14
  // Determine template path
17
15
  const templateDir = path_1.default.resolve(__dirname, '..', 'templates', template);
18
- console.log((0, colors_1.dim)(`Template source: ${templateDir}`));
19
16
  if (!fs_extra_1.default.existsSync(templateDir)) {
20
- console.error((0, colors_1.red)(`FATAL: Template source does not exist!`));
21
17
  throw new Error(`Template "${template}" not found at ${templateDir}`);
22
18
  }
23
19
  // Ensure target directory exists
24
- try {
25
- fs_extra_1.default.ensureDirSync(root);
26
- console.log((0, colors_1.dim)(`Confirmed target directory exists: ${root}`));
27
- }
28
- catch (e) {
29
- console.error((0, colors_1.red)(`Failed to create directory: ${e.message}`));
30
- }
20
+ await fs_extra_1.default.ensureDir(root);
31
21
  // Copy template files
32
22
  try {
33
- console.log((0, colors_1.dim)('Copying files...'));
34
23
  await fs_extra_1.default.copy(templateDir, root, {
35
24
  overwrite: true,
36
25
  filter: (src) => {
@@ -39,21 +28,14 @@ async function scaffold(options) {
39
28
  return !parts.includes('node_modules') && !parts.includes('.git');
40
29
  }
41
30
  });
42
- // Immediate verification
43
- const files = fs_extra_1.default.readdirSync(root);
44
- console.log((0, colors_1.dim)(`Copy complete. Files in target: ${files.join(', ')}`));
45
- if (files.length === 0) {
46
- console.error((0, colors_1.red)('WARNING: Target directory is empty after copy!'));
47
- }
48
31
  }
49
32
  catch (e) {
50
33
  console.error((0, colors_1.red)(`Failed to copy template: ${e.message}`));
51
34
  }
52
- // Rename _gitignore to .gitignore
35
+ // Restore .gitignore
53
36
  const gitignorePath = path_1.default.join(root, '_gitignore');
54
37
  if (fs_extra_1.default.existsSync(gitignorePath)) {
55
38
  await fs_extra_1.default.move(gitignorePath, path_1.default.join(root, '.gitignore'), { overwrite: true });
56
- console.log((0, colors_1.dim)('Restored .gitignore'));
57
39
  }
58
40
  // Update package.json name
59
41
  const pkgPath = path_1.default.join(root, 'package.json');
@@ -62,24 +44,18 @@ async function scaffold(options) {
62
44
  const pkg = await fs_extra_1.default.readJson(pkgPath);
63
45
  pkg.name = projectName;
64
46
  await fs_extra_1.default.writeJson(pkgPath, pkg, { spaces: 2 });
65
- console.log((0, colors_1.dim)(`Updated package.json name to ${projectName}`));
66
47
  }
67
48
  catch (e) {
68
49
  console.error((0, colors_1.red)(`Failed to update package.json: ${e.message}`));
69
50
  }
70
51
  }
71
- else {
72
- console.warn((0, colors_1.yellow)(`Warning: No package.json found at ${pkgPath}`));
73
- }
74
52
  // Initialize Git
75
53
  if (git) {
76
54
  try {
77
- console.log((0, colors_1.dim)('Initializing Git...'));
78
- // Use full path to git if possible, or just rely on shell
79
- const gitCmd = process.platform === 'win32' ? 'git' : 'git';
80
- (0, child_process_1.execSync)(`${gitCmd} init`, { cwd: root, stdio: 'ignore', shell: true, env: process.env });
81
- (0, child_process_1.execSync)(`${gitCmd} add -A`, { cwd: root, stdio: 'ignore', shell: true, env: process.env });
82
- (0, child_process_1.execSync)(`${gitCmd} commit -m "Initial commit from create-aura-app"`, { cwd: root, stdio: 'ignore', shell: true, env: process.env });
55
+ const gitCmd = 'git';
56
+ (0, child_process_1.execSync)(`${gitCmd} init`, { cwd: root, stdio: 'ignore', shell: true });
57
+ (0, child_process_1.execSync)(`${gitCmd} add -A`, { cwd: root, stdio: 'ignore', shell: true });
58
+ (0, child_process_1.execSync)(`${gitCmd} commit -m "Initial commit from create-aura-app"`, { cwd: root, stdio: 'ignore', shell: true });
83
59
  console.log((0, colors_1.dim)('Initialized a git repository.'));
84
60
  }
85
61
  catch (e) {
@@ -91,8 +67,8 @@ async function scaffold(options) {
91
67
  console.log((0, colors_1.dim)('Installing dependencies... This might take a moment.'));
92
68
  try {
93
69
  const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
94
- (0, child_process_1.execSync)(`${npmCmd} install`, { cwd: root, stdio: 'inherit', shell: true, env: process.env });
95
- console.log((0, colors_1.green)('Dependencies installed successfully.'));
70
+ (0, child_process_1.execSync)(`${npmCmd} install`, { cwd: root, stdio: 'inherit', shell: true });
71
+ console.log((0, colors_1.green)('Dependencies installed successfully.'));
96
72
  }
97
73
  catch (e) {
98
74
  console.error((0, colors_1.red)(`Dependency installation failed: ${e.message}`));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@minnai/create-aura-app",
3
- "version": "0.0.10",
3
+ "version": "0.0.12",
4
4
  "description": "Scaffolding tool for new Aura projects",
5
5
  "bin": "dist/index.js",
6
6
  "files": [
@@ -9,6 +9,9 @@
9
9
  "lint": "eslint .",
10
10
  "preview": "vite preview"
11
11
  },
12
+ "engines": {
13
+ "node": ">=22.0.0"
14
+ },
12
15
  "dependencies": {
13
16
  "@minnai/aura": "^0.1.6",
14
17
  "@monaco-editor/react": "^4.7.0",
@@ -0,0 +1,32 @@
1
+ #root {
2
+ width: 100vw;
3
+ height: 100vh;
4
+ margin: 0;
5
+ padding: 0;
6
+ }
7
+
8
+ :root {
9
+ --bg-app: #f5f5f7;
10
+ --bg-sidebar: #ffffff;
11
+ --bg-primary: #ffffff;
12
+ --bg-secondary: #f0f0f5;
13
+ --text-primary: #1d1d1f;
14
+ --text-secondary: #86868b;
15
+ --text-tertiary: #d2d2d7;
16
+ --border-color: #e5e5e5;
17
+ --border-subtle: rgba(0, 0, 0, 0.05);
18
+ --accent-primary: #007aff;
19
+ --accent-hover: #0062cc;
20
+ }
21
+
22
+ * {
23
+ box-sizing: border-box;
24
+ }
25
+
26
+ body {
27
+ margin: 0;
28
+ padding: 0;
29
+ background-color: var(--bg-app);
30
+ color: var(--text-primary);
31
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
32
+ }
@@ -0,0 +1,82 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { BrowserRouter, Routes, Route, useSearchParams } from 'react-router-dom';
3
+ import { Space, ControllerProvider, ChatInterface, createStorage } from '@minnai/aura';
4
+ import { AuraProvider } from '@minnai/aura/sdk';
5
+ import { flux } from '@minnai/aura/flux/index';
6
+ import { registerExampleAIRs } from './ambiance';
7
+ import { AnalyticsTracker } from './components/AnalyticsTracker';
8
+ import { TourGuide } from './components/TourGuide';
9
+ import { analyticsService } from './services/AnalyticsService';
10
+ import { ProjectSidebar } from './components/ProjectSidebar/ProjectSidebar';
11
+ import './App.css';
12
+
13
+ const baseAuraConfig = {
14
+ llm: {
15
+ gatewayUrl: 'https://auraproxy-7bpt7e5tua-uc.a.run.app',
16
+ proxyUrl: 'https://auraproxy-7bpt7e5tua-uc.a.run.app'
17
+ },
18
+ apiUrl: 'https://auraproxy-7bpt7e5tua-uc.a.run.app',
19
+ storage: {
20
+ documents: { driver: 'indexeddb' as any },
21
+ objects: { driver: 'indexeddb' as any }
22
+ }
23
+ };
24
+
25
+ createStorage(baseAuraConfig.storage);
26
+
27
+ const defaultProjectId = 'aura-tour-demo';
28
+
29
+ function Home() {
30
+ const [searchParams] = useSearchParams();
31
+ const projectId = searchParams.get('project') || defaultProjectId;
32
+
33
+ return (
34
+ <div style={{ display: 'flex', height: '100vh', width: '100vw', background: '#f5f5f7' }}>
35
+ {/* Reset explicit projectId passing to TourController if it reads from hook, or pass it */}
36
+ {/* We will pass it to keep it pure if possible, or render it here knowing it can use hooks */}
37
+ <TourGuide projectId={projectId} />
38
+ <ProjectSidebar currentProjectId={projectId} />
39
+ <aside style={{
40
+ width: '350px',
41
+ borderRight: '1px solid #ddd',
42
+ background: 'white',
43
+ display: 'flex',
44
+ flexDirection: 'column',
45
+ zIndex: 10
46
+ }}>
47
+ <ChatInterface
48
+ placeholder="Talk to Aura..."
49
+ />
50
+ </aside>
51
+ <main style={{ flex: 1, position: 'relative', overflow: 'hidden', background: '#fff' }}>
52
+ <Space projectId={projectId} />
53
+ </main>
54
+ </div>
55
+ );
56
+ }
57
+
58
+ function App() {
59
+ const [sessionId] = useState(() => 'sess_' + Math.random().toString(36).substr(2, 9));
60
+ const config = { ...baseAuraConfig, sessionId };
61
+
62
+ useEffect(() => {
63
+ registerExampleAIRs();
64
+ analyticsService.startTTVTimer();
65
+ (window as any).flux = flux;
66
+ }, []);
67
+
68
+ return (
69
+ <AuraProvider config={config}>
70
+ <ControllerProvider>
71
+ <BrowserRouter>
72
+ <AnalyticsTracker />
73
+ <Routes>
74
+ <Route path="/" element={<Home />} />
75
+ </Routes>
76
+ </BrowserRouter>
77
+ </ControllerProvider>
78
+ </AuraProvider>
79
+ );
80
+ }
81
+
82
+ export default App;
@@ -0,0 +1,25 @@
1
+ import React from 'react';
2
+ import { CurrencyManifest } from './manifest';
3
+ import { resources } from './resources';
4
+ import CurrencyUI from './ui';
5
+ import { useCurrencyLogic } from './logic';
6
+
7
+ const CurrencyAIR = React.forwardRef<any, any>((props, ref) => {
8
+ const logic = useCurrencyLogic({ ...props, resources });
9
+
10
+ React.useImperativeHandle(ref, () => ({
11
+ getContext: async () => ({ from: logic.from, to: logic.to, amount: logic.amount, result: logic.result }),
12
+ capabilities: ['currency-conversion']
13
+ }));
14
+
15
+ return <CurrencyUI {...logic} />;
16
+ });
17
+
18
+ export default {
19
+ manifest: CurrencyManifest,
20
+ resources,
21
+ component: CurrencyAIR
22
+ };
23
+ export { CurrencyManifest } from './manifest';
24
+ export { resources } from './resources';
25
+ export const Component = CurrencyAIR;
@@ -0,0 +1,49 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { flux } from '@minnai/aura/flux/index';
3
+
4
+ export function useCurrencyLogic(props: any) {
5
+ const [amount, setAmount] = useState(1);
6
+ const [from, setFrom] = useState('USD');
7
+ const [to, setTo] = useState('EUR');
8
+ const [result, setResult] = useState<number | null>(null);
9
+ const [loading, setLoading] = useState(false);
10
+ const [error, setError] = useState<string | null>(null);
11
+
12
+ const { resources } = props;
13
+ // BUG: This should use resources.api.currency.rates.config.url instead!
14
+ // It's currently hardcoded to the wrong file name
15
+ const API_URL = '/dollar.json'; // Should be '/usd.json'!
16
+
17
+ useEffect(() => {
18
+ convert();
19
+ }, []);
20
+
21
+ const convert = async () => {
22
+ setLoading(true);
23
+ setError(null);
24
+ try {
25
+ const response = await fetch(API_URL);
26
+ if (!response.ok) throw new Error(`Failed to fetch rates: ${response.statusText}`);
27
+ const data = await response.json();
28
+ const rate = data.rates[to];
29
+ const finalResult = amount * rate;
30
+ setResult(finalResult);
31
+ flux.dispatch({
32
+ type: 'UPDATE_STATE',
33
+ payload: { airId: 'currency-air', amount, from, to, result: finalResult },
34
+ to: 'all'
35
+ });
36
+ } catch (err: any) {
37
+ setError(err.message || "Conversion failed");
38
+ flux.dispatch({
39
+ type: 'AIR_ERROR',
40
+ payload: { airId: 'currency-air', error: err.message || "Conversion failed" },
41
+ to: 'all'
42
+ });
43
+ } finally {
44
+ setLoading(false);
45
+ }
46
+ };
47
+
48
+ return { amount, setAmount, from, setFrom, to, setTo, result, loading, error, convert };
49
+ }
@@ -0,0 +1,15 @@
1
+ export const CurrencyManifest = {
2
+ id: 'currency-air',
3
+ meta: {
4
+ title: 'Currency Converter',
5
+ icon: '💱',
6
+ description: 'Convert between different currencies.',
7
+ width: 350,
8
+ height: 400
9
+ },
10
+ instructions: {
11
+ tasks: {
12
+ 'convert': 'Convert an amount from one currency to another.'
13
+ }
14
+ }
15
+ };
@@ -0,0 +1,16 @@
1
+ // Currency conversion using local exchange rate data
2
+ export const resources = {
3
+ api: {
4
+ currency: {
5
+ rates: {
6
+ id: 'local-exchange-rates',
7
+ provider: 'static',
8
+ config: {
9
+ url: '/usd.json', // This should be 'usd.json' not 'dollar.json'!
10
+ method: 'GET',
11
+ base: 'USD'
12
+ }
13
+ }
14
+ }
15
+ }
16
+ } as const;
@@ -0,0 +1,42 @@
1
+ import React from 'react';
2
+
3
+ export default function CurrencyUI({
4
+ amount, setAmount, from, setFrom, to, setTo, result, loading, error, convert
5
+ }: any) {
6
+ return (
7
+ <div style={{ paddingTop: 0, height: '100%', display: 'flex', flexDirection: 'column' }}>
8
+ <div style={{ marginTop: 10, fontSize: '0.8rem', fontWeight: 600, textTransform: 'uppercase', color: '#888', letterSpacing: '0.5px' }}>Currency Converter</div>
9
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 15, marginTop: 20 }}>
10
+ <div style={{ display: 'flex', gap: 10 }}>
11
+ <input type="number" value={amount} onChange={(e) => setAmount(parseFloat(e.target.value))} style={{ flex: 1, padding: 8, borderRadius: 4, border: '1px solid #ddd' }} />
12
+ <select value={from} onChange={(e) => setFrom(e.target.value)} style={{ padding: 8, borderRadius: 4, border: '1px solid #ddd' }}>
13
+ <option value="USD">USD</option>
14
+ <option value="EUR">EUR</option>
15
+ </select>
16
+ </div>
17
+ <div style={{ textAlign: 'center', color: '#999' }}>⬇️</div>
18
+ <div style={{ display: 'flex', gap: 10 }}>
19
+ <div style={{ flex: 1, padding: 8, background: '#f5f5f5', borderRadius: 4 }}>{result !== null ? result.toFixed(2) : '-'}</div>
20
+ <select value={to} onChange={(e) => setTo(e.target.value)} style={{ padding: 8, borderRadius: 4, border: '1px solid #ddd' }}>
21
+ <option value="EUR">EUR</option>
22
+ <option value="USD">USD</option>
23
+ </select>
24
+ </div>
25
+ <button onClick={convert} style={{ padding: 10, background: '#007aff', color: 'white', border: 'none', borderRadius: 6, cursor: 'pointer' }}>Convert</button>
26
+ </div>
27
+ {loading && <div style={{ textAlign: 'center', marginTop: 20, color: '#666' }}>Loading...</div>}
28
+ {error && (
29
+ <div style={{ marginTop: 20, padding: 15, background: '#fff0f0', border: '1px solid #ffcccc', borderRadius: 8, color: '#d32f2f', fontSize: '0.9rem' }}>
30
+ <strong>Error:</strong> {error}
31
+ <div style={{ marginTop: 5, fontSize: '0.8rem', opacity: 0.8 }}>File not found: /dollar.json</div>
32
+ </div>
33
+ )}
34
+ {result !== null && !loading && !error && (
35
+ <div style={{ marginTop: 20, padding: 15, background: '#e8f5e9', border: '1px solid #c8e6c9', borderRadius: 8, color: '#2e7d32', fontSize: '0.9rem', textAlign: 'center' }}>
36
+ <strong>Success!</strong>
37
+ <div style={{ fontSize: '1.2rem', fontWeight: 600, marginTop: 5 }}>Result: {result.toFixed(2)} {to}</div>
38
+ </div>
39
+ )}
40
+ </div>
41
+ );
42
+ }
@@ -0,0 +1,48 @@
1
+ import { atmosphere } from '@minnai/aura/atmosphere';
2
+ import TasksAIR from '@minnai/aura/atmosphere/tasks';
3
+ import YouTubeAIR from '@minnai/aura/atmosphere/youtube-player';
4
+ import NoteTakerAIR from '@minnai/aura/atmosphere/note-taker';
5
+
6
+ // Local Ambiance
7
+ import { StocksManifest, Component as StocksComponent, resources as stocksResources } from './stocks-air';
8
+ import { CurrencyManifest, Component as CurrencyComponent, resources as currencyResources } from './currency-air';
9
+ import { LOCAL_AIRS } from '../../aura.config';
10
+
11
+ export const registerExampleAIRs = () => {
12
+ // 1. Register Standard AIRs (Core)
13
+ // The Standard AIRs export a wrapper object { manifest, resources, component }
14
+ // We need to flatten this to match AIRManifest { id, meta, component }
15
+ [TasksAIR, YouTubeAIR, NoteTakerAIR].forEach((air: any) => {
16
+ if (LOCAL_AIRS.includes(air.manifest.id)) {
17
+ atmosphere.register({
18
+ ...air.manifest,
19
+ component: air.component,
20
+ resources: air.resources
21
+ });
22
+ }
23
+ });
24
+
25
+ // 2. Register Local Demo AIRs
26
+ if (LOCAL_AIRS.includes(StocksManifest.id)) {
27
+ atmosphere.register({
28
+ ...StocksManifest,
29
+ component: StocksComponent as any,
30
+ resources: stocksResources
31
+ });
32
+ }
33
+ if (LOCAL_AIRS.includes(CurrencyManifest.id)) {
34
+ atmosphere.register({
35
+ ...CurrencyManifest,
36
+ component: CurrencyComponent as any,
37
+ resources: currencyResources
38
+ });
39
+ }
40
+ };
41
+
42
+ export const EXAMPLE_AIRS = [
43
+ NoteTakerAIR.manifest,
44
+ YouTubeAIR.manifest,
45
+ TasksAIR.manifest,
46
+ StocksManifest,
47
+ CurrencyManifest
48
+ ].filter(manifest => LOCAL_AIRS.includes(manifest.id));
@@ -0,0 +1,3 @@
1
+ export * from './manifest';
2
+ export * from './index.tsx';
3
+ export * from './resources';
@@ -0,0 +1,28 @@
1
+ import React from 'react';
2
+ import { StocksManifest } from './manifest';
3
+ import { resources } from './resources';
4
+ import StocksUI from './ui';
5
+ import { useStocksLogic } from './logic';
6
+
7
+ const StocksAIR = React.forwardRef<any, any>((props, ref) => {
8
+ const logic = useStocksLogic({ ...props, resources });
9
+
10
+ React.useImperativeHandle(ref, () => ({
11
+ getContext: async () => ({
12
+ symbol: logic.symbol,
13
+ lastPrice: logic.data?.price,
14
+ change: logic.data?.changePercent
15
+ }),
16
+ capabilities: ['market-data']
17
+ }));
18
+
19
+ return <StocksUI {...logic} onSearch={logic.fetchStock} />;
20
+ });
21
+
22
+ export default {
23
+ manifest: StocksManifest,
24
+ resources,
25
+ component: StocksAIR
26
+ };
27
+ export { StocksManifest } from './manifest';
28
+ export const Component = StocksAIR;
@@ -0,0 +1,87 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { flux } from '@minnai/aura/flux/index';
3
+
4
+ export function useStocksLogic(props: any) {
5
+ const [symbol, setSymbol] = useState(props.symbol || 'AAPL');
6
+ const [data, setData] = useState<any>(null);
7
+ const [loading, setLoading] = useState(false);
8
+ const [error, setError] = useState<string | null>(null);
9
+ const [apiKeyMissing, setApiKeyMissing] = useState(false);
10
+
11
+ const { resources } = props;
12
+ const apiKey = resources?.keys?.STOCKS_API_KEY;
13
+ const apiConfig = resources?.api?.stocks?.timeSeries?.config;
14
+
15
+ useEffect(() => {
16
+ if (!apiKey || apiKey === 'YOUR_API_KEY' || apiKey.length < 5) {
17
+ setApiKeyMissing(true);
18
+ flux.dispatch({
19
+ type: 'AIR_ERROR',
20
+ payload: {
21
+ airId: 'stocks-air',
22
+ error: '🚨 STOCKS_API_KEY is missing in resources.ts!'
23
+ },
24
+ to: 'all'
25
+ });
26
+ return;
27
+ }
28
+ fetchStock(symbol);
29
+ }, [apiKey]);
30
+
31
+ const fetchStock = async (sym: string) => {
32
+ setLoading(true);
33
+ setError(null);
34
+ try {
35
+ const url = `${apiConfig.url}?function=${apiConfig.params.function}&symbol=${sym}&apikey=${apiKey}`;
36
+ const response = await fetch(url);
37
+ const json = await response.json();
38
+
39
+ if (json['Time Series (Daily)']) {
40
+ const timeSeries = json['Time Series (Daily)'];
41
+ const dates = Object.keys(timeSeries).sort();
42
+ const latestDate = dates[dates.length - 1];
43
+ const prevDate = dates[dates.length - 2];
44
+
45
+ const latestClose = parseFloat(timeSeries[latestDate]['4. close']);
46
+ const prevClose = parseFloat(timeSeries[prevDate]['4. close']);
47
+ const change = latestClose - prevClose;
48
+ const changePercent = (change / prevClose) * 100;
49
+
50
+ const history = dates.slice(-30).map(date => ({
51
+ date: date.substring(5),
52
+ price: parseFloat(timeSeries[date]['4. close'])
53
+ }));
54
+
55
+ setData({
56
+ symbol: json['Meta Data']['2. Symbol'],
57
+ price: latestClose.toFixed(2),
58
+ change: change.toFixed(2),
59
+ changePercent: `${changePercent.toFixed(2)}%`,
60
+ history
61
+ });
62
+
63
+ flux.dispatch({
64
+ type: 'UPDATE_STATE',
65
+ payload: { airId: 'stocks-air', price: latestClose.toFixed(2) },
66
+ to: 'all'
67
+ });
68
+ } else if (json['Note']) {
69
+ const msg = "API limit reached.";
70
+ setError(msg);
71
+ flux.dispatch({ type: 'AIR_ERROR', payload: { airId: 'stocks-air', error: msg }, to: 'all' });
72
+ } else {
73
+ const msg = "Symbol not found.";
74
+ setError(msg);
75
+ flux.dispatch({ type: 'AIR_ERROR', payload: { airId: 'stocks-air', error: msg }, to: 'all' });
76
+ }
77
+ } catch (err) {
78
+ const msg = "Network error.";
79
+ setError(msg);
80
+ flux.dispatch({ type: 'AIR_ERROR', payload: { airId: 'stocks-air', error: msg }, to: 'all' });
81
+ } finally {
82
+ setLoading(false);
83
+ }
84
+ };
85
+
86
+ return { symbol, setSymbol, data, loading, error, apiKeyMissing, fetchStock };
87
+ }
@@ -0,0 +1,15 @@
1
+ export const StocksManifest = {
2
+ id: 'stocks-air',
3
+ meta: {
4
+ title: 'Stock Tracker',
5
+ icon: '📈',
6
+ description: 'Track real-time stock prices.',
7
+ width: 350,
8
+ height: 400
9
+ },
10
+ instructions: {
11
+ tasks: {
12
+ 'check_price': 'Check the stock price for a given symbol.'
13
+ }
14
+ }
15
+ };
@@ -0,0 +1,23 @@
1
+ // Add your AlphaVantage API key here
2
+ // Get a free key at: https://www.alphavantage.co/support/#api-key
3
+ export const resources = {
4
+ api: {
5
+ stocks: {
6
+ timeSeries: {
7
+ id: 'alphavantage-timeseries',
8
+ provider: 'proxy',
9
+ config: {
10
+ url: 'https://www.alphavantage.co/query',
11
+ method: 'GET',
12
+ auth: 'STOCKS_API_KEY',
13
+ params: {
14
+ function: 'TIME_SERIES_DAILY'
15
+ }
16
+ }
17
+ }
18
+ }
19
+ },
20
+ keys: {
21
+ STOCKS_API_KEY: 'PUT YOUR API KEY HERE'
22
+ }
23
+ } as const;
@@ -0,0 +1,67 @@
1
+ import React from 'react';
2
+ import { LineChart, Line, ResponsiveContainer, YAxis, Tooltip } from 'recharts';
3
+
4
+ export default function StocksUI({
5
+ symbol, setSymbol, data, loading, error, apiKeyMissing, onSearch
6
+ }: any) {
7
+ const isPositive = data && parseFloat(data.change) >= 0;
8
+
9
+ return (
10
+ <div style={{ paddingTop: 0, height: '100%', display: 'flex', flexDirection: 'column' }}>
11
+ <div style={{ marginTop: 10, fontSize: '0.8rem', fontWeight: 600, textTransform: 'uppercase', color: '#888', letterSpacing: '0.5px' }}>Market Data</div>
12
+
13
+ {apiKeyMissing ? (
14
+ <div style={{ padding: 20, background: '#fff0f0', borderRadius: 8, border: '1px solid #ffcccc', color: '#d32f2f' }}>
15
+ <strong>API Key Missing</strong>
16
+ <p style={{ margin: '10px 0', fontSize: '0.9rem' }}>Please add <code>STOCK_API_KEY</code> to your environment variables.</p>
17
+ </div>
18
+ ) : (
19
+ <>
20
+ <form onSubmit={(e) => { e.preventDefault(); onSearch(symbol); }} style={{ marginBottom: 10 }}>
21
+ <input
22
+ value={symbol}
23
+ onChange={(e) => setSymbol(e.target.value.toUpperCase())}
24
+ placeholder="Enter Symbol (e.g. AAPL)"
25
+ style={{ width: '100%', padding: '10px', borderRadius: 8, border: '1px solid #ddd', outline: 'none' }}
26
+ />
27
+ </form>
28
+
29
+ {loading && <div style={{ textAlign: 'center', color: '#666' }}>Loading...</div>}
30
+ {error && <div style={{ color: 'red', textAlign: 'center' }}>{error}</div>}
31
+
32
+ {data && !loading && (
33
+ <div style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
34
+ <div style={{ textAlign: 'center', marginBottom: 10 }}>
35
+ <h2 style={{ fontSize: '2rem', margin: '0 0 5px 0', fontWeight: 800 }}>{data.price}</h2>
36
+ <div style={{ fontSize: '1.2rem', color: isPositive ? '#00c853' : '#d32f2f', fontWeight: 600 }}>
37
+ {isPositive ? '▲' : '▼'} {data.change} ({data.changePercent})
38
+ </div>
39
+ <div style={{ color: '#999', fontSize: '0.8rem' }}>{data.symbol} - Last 30 Days</div>
40
+ </div>
41
+
42
+ <div style={{ flex: 1, minHeight: 150, width: '100%' }}>
43
+ <ResponsiveContainer width="100%" height="100%">
44
+ <LineChart data={data.history}>
45
+ <YAxis domain={['auto', 'auto']} hide={true} />
46
+ <Tooltip
47
+ contentStyle={{ borderRadius: 8, border: 'none', boxShadow: '0 2px 10px rgba(0,0,0,0.1)' }}
48
+ itemStyle={{ color: '#333' }}
49
+ formatter={(value: any) => [parseFloat(value).toFixed(2), 'Price']}
50
+ />
51
+ <Line
52
+ type="monotone"
53
+ dataKey="price"
54
+ stroke={isPositive ? '#00c853' : '#d32f2f'}
55
+ strokeWidth={2}
56
+ dot={false}
57
+ />
58
+ </LineChart>
59
+ </ResponsiveContainer>
60
+ </div>
61
+ </div>
62
+ )}
63
+ </>
64
+ )}
65
+ </div>
66
+ );
67
+ }