KevIsDev
commited on
Commit
·
23c22c5
1
Parent(s):
002f1bc
fix: show netlify deployed link
Browse filesnetlify deploy button to be disabled on streaming and show link icon when deployed
- app/components/@settings/tabs/connections/GithubConnection.tsx +134 -159
- app/components/@settings/tabs/connections/NetlifyConnection.tsx +8 -36
- app/components/chat/BaseChat.tsx +7 -1
- app/components/chat/Chat.client.tsx +4 -0
- app/components/chat/ChatAlert.tsx +1 -1
- app/components/chat/NetlifyDeploymentLink.client.tsx +51 -0
- app/components/chat/ProgressCompilation.tsx +0 -1
- app/components/header/HeaderActionButtons.client.tsx +9 -5
- app/lib/stores/netlify.ts +36 -0
- app/lib/stores/streaming.ts +3 -0
app/components/@settings/tabs/connections/GithubConnection.tsx
CHANGED
@@ -73,22 +73,13 @@ export function GithubConnection() {
|
|
73 |
});
|
74 |
const [isLoading, setIsLoading] = useState(true);
|
75 |
const [isConnecting, setIsConnecting] = useState(false);
|
76 |
-
const [
|
77 |
-
|
78 |
-
languages: false,
|
79 |
-
recentActivity: false,
|
80 |
-
repositories: false,
|
81 |
-
});
|
82 |
-
|
83 |
-
const toggleSection = (section: keyof typeof expandedSections) => {
|
84 |
-
setExpandedSections((prev) => ({
|
85 |
-
...prev,
|
86 |
-
[section]: !prev[section],
|
87 |
-
}));
|
88 |
-
};
|
89 |
|
90 |
const fetchGitHubStats = async (token: string) => {
|
91 |
try {
|
|
|
|
|
92 |
const reposResponse = await fetch(
|
93 |
'https://api.github.com/user/repos?sort=updated&per_page=10&affiliation=owner,organization_member,collaborator',
|
94 |
{
|
@@ -165,6 +156,7 @@ export function GithubConnection() {
|
|
165 |
logStore.logError('Failed to fetch GitHub stats', { error });
|
166 |
toast.error('Failed to fetch GitHub statistics');
|
167 |
} finally {
|
|
|
168 |
}
|
169 |
};
|
170 |
|
@@ -188,7 +180,7 @@ export function GithubConnection() {
|
|
188 |
setIsLoading(false);
|
189 |
}, []);
|
190 |
|
191 |
-
if (isLoading) {
|
192 |
return <LoadingSpinner />;
|
193 |
}
|
194 |
|
@@ -350,7 +342,7 @@ export function GithubConnection() {
|
|
350 |
'hover:bg-red-600',
|
351 |
)}
|
352 |
>
|
353 |
-
<div className="i-ph:plug w-4 h-4" />
|
354 |
Disconnect
|
355 |
</button>
|
356 |
)}
|
@@ -365,161 +357,144 @@ export function GithubConnection() {
|
|
365 |
|
366 |
{connection.user && connection.stats && (
|
367 |
<div className="mt-6 border-t border-[#E5E5E5] dark:border-[#1A1A1A] pt-6">
|
368 |
-
<
|
369 |
-
<
|
370 |
-
|
371 |
-
<
|
372 |
-
|
373 |
-
|
374 |
-
|
375 |
-
|
376 |
-
|
377 |
-
|
378 |
-
|
379 |
-
|
380 |
-
|
381 |
-
|
382 |
-
<span className="flex items-center gap-1">
|
383 |
-
<div className="i-ph:book-bookmark w-4 h-4" />
|
384 |
-
{connection.user.public_repos} public repos
|
385 |
-
</span>
|
386 |
-
<span className="flex items-center gap-1">
|
387 |
-
<div className="i-ph:star w-4 h-4" />
|
388 |
-
{connection.stats.totalStars} stars
|
389 |
-
</span>
|
390 |
-
<span className="flex items-center gap-1">
|
391 |
-
<div className="i-ph:git-fork w-4 h-4" />
|
392 |
-
{connection.stats.totalForks} forks
|
393 |
-
</span>
|
394 |
-
</div>
|
395 |
-
</div>
|
396 |
-
</div>
|
397 |
-
|
398 |
-
{/* Organizations Section */}
|
399 |
-
{connection.stats.organizations.length > 0 && (
|
400 |
-
<div className="space-y-3">
|
401 |
-
<button
|
402 |
-
onClick={() => toggleSection('organizations')}
|
403 |
-
className="w-full bg-transparent text-left text-sm font-medium text-bolt-elements-textPrimary flex items-center gap-2"
|
404 |
-
>
|
405 |
-
<div className="i-ph:buildings w-4 h-4" />
|
406 |
-
Organizations ({connection.stats.organizations.length})
|
407 |
-
<div
|
408 |
-
className={classNames(
|
409 |
-
'i-ph:caret-down w-4 h-4 ml-auto transition-transform',
|
410 |
-
expandedSections.organizations ? 'rotate-180' : '',
|
411 |
-
)}
|
412 |
-
/>
|
413 |
-
</button>
|
414 |
-
{expandedSections.organizations && (
|
415 |
-
<div className="flex flex-wrap gap-3 pb-4">
|
416 |
-
{connection.stats.organizations.map((org) => (
|
417 |
-
<a
|
418 |
-
key={org.login}
|
419 |
-
href={org.html_url}
|
420 |
-
target="_blank"
|
421 |
-
rel="noopener noreferrer"
|
422 |
-
className="flex items-center gap-2 p-2 rounded-lg bg-[#F8F8F8] dark:bg-[#1A1A1A] hover:bg-[#F0F0F0] dark:hover:bg-[#252525] transition-colors"
|
423 |
-
>
|
424 |
-
<img src={org.avatar_url} alt={org.login} className="w-6 h-6 rounded-md" />
|
425 |
-
<span className="text-sm text-bolt-elements-textPrimary">{org.login}</span>
|
426 |
-
</a>
|
427 |
-
))}
|
428 |
</div>
|
429 |
-
|
430 |
-
|
431 |
-
)}
|
432 |
-
|
433 |
-
{/* Languages Section */}
|
434 |
-
<div className="space-y-3">
|
435 |
-
<button
|
436 |
-
onClick={() => toggleSection('languages')}
|
437 |
-
className="w-full bg-transparent text-left text-sm font-medium text-bolt-elements-textPrimary flex items-center gap-2"
|
438 |
-
>
|
439 |
-
<div className="i-ph:code w-4 h-4" />
|
440 |
-
Top Languages ({Object.keys(connection.stats.languages).length})
|
441 |
-
<div
|
442 |
-
className={classNames(
|
443 |
-
'i-ph:caret-down w-4 h-4 ml-auto transition-transform',
|
444 |
-
expandedSections.languages ? 'rotate-180' : '',
|
445 |
)}
|
446 |
-
|
447 |
-
|
448 |
-
|
449 |
-
|
450 |
-
|
451 |
-
|
452 |
-
|
453 |
-
|
454 |
-
|
455 |
-
|
456 |
-
|
457 |
-
|
458 |
-
|
459 |
-
|
460 |
-
|
|
|
|
|
|
|
461 |
</div>
|
462 |
-
|
463 |
-
</
|
464 |
|
465 |
-
{
|
466 |
-
|
467 |
-
|
468 |
-
|
469 |
-
|
470 |
-
|
471 |
-
|
472 |
-
Recent Activity ({connection.stats.recentActivity.length})
|
473 |
-
<div
|
474 |
-
className={classNames(
|
475 |
-
'i-ph:caret-down w-4 h-4 ml-auto transition-transform',
|
476 |
-
expandedSections.recentActivity ? 'rotate-180' : '',
|
477 |
-
)}
|
478 |
-
/>
|
479 |
-
</button>
|
480 |
-
{expandedSections.recentActivity && (
|
481 |
-
<div className="space-y-3 pb-4">
|
482 |
-
{connection.stats.recentActivity.map((event) => (
|
483 |
-
<div key={event.id} className="p-3 rounded-lg bg-[#F8F8F8] dark:bg-[#1A1A1A] text-sm">
|
484 |
-
<div className="flex items-center gap-2 text-bolt-elements-textPrimary">
|
485 |
-
<div className="i-ph:git-commit w-4 h-4 text-bolt-elements-textSecondary" />
|
486 |
-
<span className="font-medium">{event.type.replace('Event', '')}</span>
|
487 |
-
<span>on</span>
|
488 |
<a
|
489 |
-
|
|
|
490 |
target="_blank"
|
491 |
rel="noopener noreferrer"
|
492 |
-
className="
|
493 |
>
|
494 |
-
{
|
|
|
495 |
</a>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
496 |
</div>
|
497 |
-
|
498 |
-
|
499 |
-
|
500 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
501 |
</div>
|
502 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
503 |
</div>
|
504 |
-
)}
|
505 |
-
</div>
|
506 |
|
507 |
-
|
508 |
-
|
509 |
-
<button
|
510 |
-
onClick={() => toggleSection('repositories')}
|
511 |
-
className="w-full bg-transparent text-left text-sm font-medium text-bolt-elements-textPrimary flex items-center gap-2"
|
512 |
-
>
|
513 |
-
<div className="i-ph:clock-counter-clockwise w-4 h-4" />
|
514 |
-
Recent Repositories ({connection.stats.repos.length})
|
515 |
-
<div
|
516 |
-
className={classNames(
|
517 |
-
'i-ph:caret-down w-4 h-4 ml-auto transition-transform',
|
518 |
-
expandedSections.repositories ? 'rotate-180' : '',
|
519 |
-
)}
|
520 |
-
/>
|
521 |
-
</button>
|
522 |
-
{expandedSections.repositories && (
|
523 |
<div className="space-y-3">
|
524 |
{connection.stats.repos.map((repo) => (
|
525 |
<a
|
@@ -561,8 +536,8 @@ export function GithubConnection() {
|
|
561 |
</a>
|
562 |
))}
|
563 |
</div>
|
564 |
-
|
565 |
-
|
566 |
</div>
|
567 |
)}
|
568 |
</div>
|
|
|
73 |
});
|
74 |
const [isLoading, setIsLoading] = useState(true);
|
75 |
const [isConnecting, setIsConnecting] = useState(false);
|
76 |
+
const [isFetchingStats, setIsFetchingStats] = useState(false);
|
77 |
+
const [isStatsExpanded, setIsStatsExpanded] = useState(false);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
78 |
|
79 |
const fetchGitHubStats = async (token: string) => {
|
80 |
try {
|
81 |
+
setIsFetchingStats(true);
|
82 |
+
|
83 |
const reposResponse = await fetch(
|
84 |
'https://api.github.com/user/repos?sort=updated&per_page=10&affiliation=owner,organization_member,collaborator',
|
85 |
{
|
|
|
156 |
logStore.logError('Failed to fetch GitHub stats', { error });
|
157 |
toast.error('Failed to fetch GitHub statistics');
|
158 |
} finally {
|
159 |
+
setIsFetchingStats(false);
|
160 |
}
|
161 |
};
|
162 |
|
|
|
180 |
setIsLoading(false);
|
181 |
}, []);
|
182 |
|
183 |
+
if (isLoading || isConnecting || isFetchingStats) {
|
184 |
return <LoadingSpinner />;
|
185 |
}
|
186 |
|
|
|
342 |
'hover:bg-red-600',
|
343 |
)}
|
344 |
>
|
345 |
+
<div className="i-ph:plug-x w-4 h-4" />
|
346 |
Disconnect
|
347 |
</button>
|
348 |
)}
|
|
|
357 |
|
358 |
{connection.user && connection.stats && (
|
359 |
<div className="mt-6 border-t border-[#E5E5E5] dark:border-[#1A1A1A] pt-6">
|
360 |
+
<button onClick={() => setIsStatsExpanded(!isStatsExpanded)} className="w-full bg-transparent">
|
361 |
+
<div className="flex items-center gap-4">
|
362 |
+
<img src={connection.user.avatar_url} alt={connection.user.login} className="w-16 h-16 rounded-full" />
|
363 |
+
<div className="flex-1">
|
364 |
+
<div className="flex items-center justify-between">
|
365 |
+
<h3 className="text-lg font-medium text-bolt-elements-textPrimary">
|
366 |
+
{connection.user.name || connection.user.login}
|
367 |
+
</h3>
|
368 |
+
<div
|
369 |
+
className={classNames(
|
370 |
+
'i-ph:caret-down w-4 h-4 text-bolt-elements-textSecondary transition-transform',
|
371 |
+
isStatsExpanded ? 'rotate-180' : '',
|
372 |
+
)}
|
373 |
+
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
374 |
</div>
|
375 |
+
{connection.user.bio && (
|
376 |
+
<p className="text-sm text-start text-bolt-elements-textSecondary">{connection.user.bio}</p>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
377 |
)}
|
378 |
+
<div className="flex gap-4 mt-2 text-sm text-bolt-elements-textSecondary">
|
379 |
+
<span className="flex items-center gap-1">
|
380 |
+
<div className="i-ph:users w-4 h-4" />
|
381 |
+
{connection.user.followers} followers
|
382 |
+
</span>
|
383 |
+
<span className="flex items-center gap-1">
|
384 |
+
<div className="i-ph:book-bookmark w-4 h-4" />
|
385 |
+
{connection.user.public_repos} public repos
|
386 |
+
</span>
|
387 |
+
<span className="flex items-center gap-1">
|
388 |
+
<div className="i-ph:star w-4 h-4" />
|
389 |
+
{connection.stats.totalStars} stars
|
390 |
+
</span>
|
391 |
+
<span className="flex items-center gap-1">
|
392 |
+
<div className="i-ph:git-fork w-4 h-4" />
|
393 |
+
{connection.stats.totalForks} forks
|
394 |
+
</span>
|
395 |
+
</div>
|
396 |
</div>
|
397 |
+
</div>
|
398 |
+
</button>
|
399 |
|
400 |
+
{isStatsExpanded && (
|
401 |
+
<div className="pt-4">
|
402 |
+
{connection.stats.organizations.length > 0 && (
|
403 |
+
<div className="mb-6">
|
404 |
+
<h4 className="text-sm font-medium text-bolt-elements-textPrimary mb-3">Organizations</h4>
|
405 |
+
<div className="flex flex-wrap gap-3">
|
406 |
+
{connection.stats.organizations.map((org) => (
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
407 |
<a
|
408 |
+
key={org.login}
|
409 |
+
href={org.html_url}
|
410 |
target="_blank"
|
411 |
rel="noopener noreferrer"
|
412 |
+
className="flex items-center gap-2 p-2 rounded-lg bg-[#F8F8F8] dark:bg-[#1A1A1A] hover:bg-[#F0F0F0] dark:hover:bg-[#252525] transition-colors"
|
413 |
>
|
414 |
+
<img src={org.avatar_url} alt={org.login} className="w-6 h-6 rounded-md" />
|
415 |
+
<span className="text-sm text-bolt-elements-textPrimary">{org.login}</span>
|
416 |
</a>
|
417 |
+
))}
|
418 |
+
</div>
|
419 |
+
</div>
|
420 |
+
)}
|
421 |
+
|
422 |
+
{/* Languages Section */}
|
423 |
+
<div className="mb-6">
|
424 |
+
<h4 className="text-sm font-medium text-bolt-elements-textPrimary mb-3">Top Languages</h4>
|
425 |
+
<div className="flex flex-wrap gap-2">
|
426 |
+
{Object.entries(connection.stats.languages)
|
427 |
+
.sort(([, a], [, b]) => b - a)
|
428 |
+
.slice(0, 5)
|
429 |
+
.map(([language]) => (
|
430 |
+
<span
|
431 |
+
key={language}
|
432 |
+
className="px-3 py-1 text-xs rounded-full bg-purple-500/10 text-purple-500 dark:bg-purple-500/20"
|
433 |
+
>
|
434 |
+
{language}
|
435 |
+
</span>
|
436 |
+
))}
|
437 |
+
</div>
|
438 |
+
</div>
|
439 |
+
|
440 |
+
{/* Recent Activity Section */}
|
441 |
+
<div className="mb-6">
|
442 |
+
<h4 className="text-sm font-medium text-bolt-elements-textPrimary mb-3">Recent Activity</h4>
|
443 |
+
<div className="space-y-3">
|
444 |
+
{connection.stats.recentActivity.map((event) => (
|
445 |
+
<div key={event.id} className="p-3 rounded-lg bg-[#F8F8F8] dark:bg-[#1A1A1A] text-sm">
|
446 |
+
<div className="flex items-center gap-2 text-bolt-elements-textPrimary">
|
447 |
+
<div className="i-ph:git-commit w-4 h-4 text-bolt-elements-textSecondary" />
|
448 |
+
<span className="font-medium">{event.type.replace('Event', '')}</span>
|
449 |
+
<span>on</span>
|
450 |
+
<a
|
451 |
+
href={`https://github.com/${event.repo.name}`}
|
452 |
+
target="_blank"
|
453 |
+
rel="noopener noreferrer"
|
454 |
+
className="text-purple-500 hover:underline"
|
455 |
+
>
|
456 |
+
{event.repo.name}
|
457 |
+
</a>
|
458 |
+
</div>
|
459 |
+
<div className="mt-1 text-xs text-bolt-elements-textSecondary">
|
460 |
+
{new Date(event.created_at).toLocaleDateString()} at{' '}
|
461 |
+
{new Date(event.created_at).toLocaleTimeString()}
|
462 |
+
</div>
|
463 |
</div>
|
464 |
+
))}
|
465 |
+
</div>
|
466 |
+
</div>
|
467 |
+
|
468 |
+
{/* Additional Stats */}
|
469 |
+
<div className="grid grid-cols-4 gap-4 mb-6">
|
470 |
+
<div className="p-4 rounded-lg bg-[#F8F8F8] dark:bg-[#1A1A1A]">
|
471 |
+
<div className="text-sm text-bolt-elements-textSecondary">Member Since</div>
|
472 |
+
<div className="text-lg font-medium text-bolt-elements-textPrimary">
|
473 |
+
{new Date(connection.user.created_at).toLocaleDateString()}
|
474 |
</div>
|
475 |
+
</div>
|
476 |
+
<div className="p-4 rounded-lg bg-[#F8F8F8] dark:bg-[#1A1A1A]">
|
477 |
+
<div className="text-sm text-bolt-elements-textSecondary">Public Gists</div>
|
478 |
+
<div className="text-lg font-medium text-bolt-elements-textPrimary">
|
479 |
+
{connection.stats.totalGists}
|
480 |
+
</div>
|
481 |
+
</div>
|
482 |
+
<div className="p-4 rounded-lg bg-[#F8F8F8] dark:bg-[#1A1A1A]">
|
483 |
+
<div className="text-sm text-bolt-elements-textSecondary">Organizations</div>
|
484 |
+
<div className="text-lg font-medium text-bolt-elements-textPrimary">
|
485 |
+
{connection.stats.organizations.length}
|
486 |
+
</div>
|
487 |
+
</div>
|
488 |
+
<div className="p-4 rounded-lg bg-[#F8F8F8] dark:bg-[#1A1A1A]">
|
489 |
+
<div className="text-sm text-bolt-elements-textSecondary">Languages</div>
|
490 |
+
<div className="text-lg font-medium text-bolt-elements-textPrimary">
|
491 |
+
{Object.keys(connection.stats.languages).length}
|
492 |
+
</div>
|
493 |
+
</div>
|
494 |
</div>
|
|
|
|
|
495 |
|
496 |
+
{/* Repositories Section */}
|
497 |
+
<h4 className="text-sm font-medium text-bolt-elements-textPrimary mb-3">Recent Repositories</h4>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
498 |
<div className="space-y-3">
|
499 |
{connection.stats.repos.map((repo) => (
|
500 |
<a
|
|
|
536 |
</a>
|
537 |
))}
|
538 |
</div>
|
539 |
+
</div>
|
540 |
+
)}
|
541 |
</div>
|
542 |
)}
|
543 |
</div>
|
app/components/@settings/tabs/connections/NetlifyConnection.tsx
CHANGED
@@ -4,8 +4,14 @@ import { toast } from 'react-toastify';
|
|
4 |
import { useStore } from '@nanostores/react';
|
5 |
import { logStore } from '~/lib/stores/logs';
|
6 |
import { classNames } from '~/utils/classNames';
|
7 |
-
import {
|
8 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
9 |
|
10 |
export function NetlifyConnection() {
|
11 |
const connection = useStore(netlifyConnection);
|
@@ -22,40 +28,6 @@ export function NetlifyConnection() {
|
|
22 |
fetchSites();
|
23 |
}, [connection.user, connection.token]);
|
24 |
|
25 |
-
const fetchNetlifyStats = async (token: string) => {
|
26 |
-
try {
|
27 |
-
isFetchingStats.set(true);
|
28 |
-
|
29 |
-
const sitesResponse = await fetch('https://api.netlify.com/api/v1/sites', {
|
30 |
-
headers: {
|
31 |
-
Authorization: `Bearer ${token}`,
|
32 |
-
'Content-Type': 'application/json',
|
33 |
-
},
|
34 |
-
});
|
35 |
-
|
36 |
-
if (!sitesResponse.ok) {
|
37 |
-
throw new Error(`Failed to fetch sites: ${sitesResponse.status}`);
|
38 |
-
}
|
39 |
-
|
40 |
-
const sites = (await sitesResponse.json()) as NetlifySite[];
|
41 |
-
|
42 |
-
const currentState = netlifyConnection.get();
|
43 |
-
updateNetlifyConnection({
|
44 |
-
...currentState,
|
45 |
-
stats: {
|
46 |
-
sites,
|
47 |
-
totalSites: sites.length,
|
48 |
-
},
|
49 |
-
});
|
50 |
-
} catch (error) {
|
51 |
-
console.error('Netlify API Error:', error);
|
52 |
-
logStore.logError('Failed to fetch Netlify stats', { error });
|
53 |
-
toast.error('Failed to fetch Netlify statistics');
|
54 |
-
} finally {
|
55 |
-
isFetchingStats.set(false);
|
56 |
-
}
|
57 |
-
};
|
58 |
-
|
59 |
const handleConnect = async (event: React.FormEvent) => {
|
60 |
event.preventDefault();
|
61 |
isConnecting.set(true);
|
|
|
4 |
import { useStore } from '@nanostores/react';
|
5 |
import { logStore } from '~/lib/stores/logs';
|
6 |
import { classNames } from '~/utils/classNames';
|
7 |
+
import {
|
8 |
+
netlifyConnection,
|
9 |
+
isConnecting,
|
10 |
+
isFetchingStats,
|
11 |
+
updateNetlifyConnection,
|
12 |
+
fetchNetlifyStats,
|
13 |
+
} from '~/lib/stores/netlify';
|
14 |
+
import type { NetlifyUser } from '~/types/netlify';
|
15 |
|
16 |
export function NetlifyConnection() {
|
17 |
const connection = useStore(netlifyConnection);
|
|
|
28 |
fetchSites();
|
29 |
}, [connection.user, connection.token]);
|
30 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
31 |
const handleConnect = async (event: React.FormEvent) => {
|
32 |
event.preventDefault();
|
33 |
isConnecting.set(true);
|
app/components/chat/BaseChat.tsx
CHANGED
@@ -45,6 +45,7 @@ interface BaseChatProps {
|
|
45 |
showChat?: boolean;
|
46 |
chatStarted?: boolean;
|
47 |
isStreaming?: boolean;
|
|
|
48 |
messages?: Message[];
|
49 |
description?: string;
|
50 |
enhancingPrompt?: boolean;
|
@@ -79,6 +80,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|
79 |
showChat = true,
|
80 |
chatStarted = false,
|
81 |
isStreaming = false,
|
|
|
82 |
model,
|
83 |
setModel,
|
84 |
provider,
|
@@ -126,6 +128,10 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|
126 |
console.log(transcript);
|
127 |
}, [transcript]);
|
128 |
|
|
|
|
|
|
|
|
|
129 |
useEffect(() => {
|
130 |
if (typeof window !== 'undefined' && ('SpeechRecognition' in window || 'webkitSpeechRecognition' in window)) {
|
131 |
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
@@ -337,7 +343,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|
337 |
}}
|
338 |
</ClientOnly>
|
339 |
<div
|
340 |
-
className={classNames('flex flex-col
|
341 |
'sticky bottom-2': chatStarted,
|
342 |
'position-absolute': chatStarted,
|
343 |
})}
|
|
|
45 |
showChat?: boolean;
|
46 |
chatStarted?: boolean;
|
47 |
isStreaming?: boolean;
|
48 |
+
onStreamingChange?: (streaming: boolean) => void;
|
49 |
messages?: Message[];
|
50 |
description?: string;
|
51 |
enhancingPrompt?: boolean;
|
|
|
80 |
showChat = true,
|
81 |
chatStarted = false,
|
82 |
isStreaming = false,
|
83 |
+
onStreamingChange,
|
84 |
model,
|
85 |
setModel,
|
86 |
provider,
|
|
|
128 |
console.log(transcript);
|
129 |
}, [transcript]);
|
130 |
|
131 |
+
useEffect(() => {
|
132 |
+
onStreamingChange?.(isStreaming);
|
133 |
+
}, [isStreaming, onStreamingChange]);
|
134 |
+
|
135 |
useEffect(() => {
|
136 |
if (typeof window !== 'undefined' && ('SpeechRecognition' in window || 'webkitSpeechRecognition' in window)) {
|
137 |
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
|
|
343 |
}}
|
344 |
</ClientOnly>
|
345 |
<div
|
346 |
+
className={classNames('flex flex-col w-full max-w-chat mx-auto z-prompt', {
|
347 |
'sticky bottom-2': chatStarted,
|
348 |
'position-absolute': chatStarted,
|
349 |
})}
|
app/components/chat/Chat.client.tsx
CHANGED
@@ -24,6 +24,7 @@ import { useSearchParams } from '@remix-run/react';
|
|
24 |
import { createSampler } from '~/utils/sampler';
|
25 |
import { getTemplates, selectStarterTemplate } from '~/utils/selectStarterTemplate';
|
26 |
import { logStore } from '~/lib/stores/logs';
|
|
|
27 |
|
28 |
const toastAnimation = cssTransition({
|
29 |
enter: 'animated fadeInRight',
|
@@ -465,6 +466,9 @@ export const ChatImpl = memo(
|
|
465 |
showChat={showChat}
|
466 |
chatStarted={chatStarted}
|
467 |
isStreaming={isLoading || fakeLoading}
|
|
|
|
|
|
|
468 |
enhancingPrompt={enhancingPrompt}
|
469 |
promptEnhanced={promptEnhanced}
|
470 |
sendMessage={sendMessage}
|
|
|
24 |
import { createSampler } from '~/utils/sampler';
|
25 |
import { getTemplates, selectStarterTemplate } from '~/utils/selectStarterTemplate';
|
26 |
import { logStore } from '~/lib/stores/logs';
|
27 |
+
import { streamingState } from '~/lib/stores/streaming';
|
28 |
|
29 |
const toastAnimation = cssTransition({
|
30 |
enter: 'animated fadeInRight',
|
|
|
466 |
showChat={showChat}
|
467 |
chatStarted={chatStarted}
|
468 |
isStreaming={isLoading || fakeLoading}
|
469 |
+
onStreamingChange={(streaming) => {
|
470 |
+
streamingState.set(streaming);
|
471 |
+
}}
|
472 |
enhancingPrompt={enhancingPrompt}
|
473 |
promptEnhanced={promptEnhanced}
|
474 |
sendMessage={sendMessage}
|
app/components/chat/ChatAlert.tsx
CHANGED
@@ -24,7 +24,7 @@ export default function ChatAlert({ alert, clearAlert, postMessage }: Props) {
|
|
24 |
animate={{ opacity: 1, y: 0 }}
|
25 |
exit={{ opacity: 0, y: -20 }}
|
26 |
transition={{ duration: 0.3 }}
|
27 |
-
className={`rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-background-depth-2 p-4`}
|
28 |
>
|
29 |
<div className="flex items-start">
|
30 |
{/* Icon */}
|
|
|
24 |
animate={{ opacity: 1, y: 0 }}
|
25 |
exit={{ opacity: 0, y: -20 }}
|
26 |
transition={{ duration: 0.3 }}
|
27 |
+
className={`rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-background-depth-2 p-4 mb-2`}
|
28 |
>
|
29 |
<div className="flex items-start">
|
30 |
{/* Icon */}
|
app/components/chat/NetlifyDeploymentLink.client.tsx
ADDED
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { useStore } from '@nanostores/react';
|
2 |
+
import { netlifyConnection, fetchNetlifyStats } from '~/lib/stores/netlify';
|
3 |
+
import { chatId } from '~/lib/persistence/useChatHistory';
|
4 |
+
import * as Tooltip from '@radix-ui/react-tooltip';
|
5 |
+
import { useEffect } from 'react';
|
6 |
+
|
7 |
+
export function NetlifyDeploymentLink() {
|
8 |
+
const connection = useStore(netlifyConnection);
|
9 |
+
const currentChatId = useStore(chatId);
|
10 |
+
|
11 |
+
useEffect(() => {
|
12 |
+
if (connection.token && currentChatId) {
|
13 |
+
fetchNetlifyStats(connection.token);
|
14 |
+
}
|
15 |
+
}, [connection.token, currentChatId]);
|
16 |
+
|
17 |
+
const deployedSite = connection.stats?.sites?.find((site) => site.name.includes(`bolt-diy-${currentChatId}`));
|
18 |
+
|
19 |
+
if (!deployedSite) {
|
20 |
+
return null;
|
21 |
+
}
|
22 |
+
|
23 |
+
return (
|
24 |
+
<Tooltip.Provider>
|
25 |
+
<Tooltip.Root>
|
26 |
+
<Tooltip.Trigger asChild>
|
27 |
+
<a
|
28 |
+
href={deployedSite.url}
|
29 |
+
target="_blank"
|
30 |
+
rel="noopener noreferrer"
|
31 |
+
className="inline-flex items-center justify-center w-8 h-8 rounded hover:bg-bolt-elements-item-backgroundActive text-bolt-elements-textSecondary hover:text-[#00AD9F] z-50"
|
32 |
+
onClick={(e) => {
|
33 |
+
e.stopPropagation(); // Add this to prevent click from bubbling up
|
34 |
+
}}
|
35 |
+
>
|
36 |
+
<div className="i-ph:rocket-launch w-5 h-5" />
|
37 |
+
</a>
|
38 |
+
</Tooltip.Trigger>
|
39 |
+
<Tooltip.Portal>
|
40 |
+
<Tooltip.Content
|
41 |
+
className="px-3 py-2 rounded bg-bolt-elements-background-depth-3 text-bolt-elements-textPrimary text-xs z-50"
|
42 |
+
sideOffset={5}
|
43 |
+
>
|
44 |
+
{deployedSite.url}
|
45 |
+
<Tooltip.Arrow className="fill-bolt-elements-background-depth-3" />
|
46 |
+
</Tooltip.Content>
|
47 |
+
</Tooltip.Portal>
|
48 |
+
</Tooltip.Root>
|
49 |
+
</Tooltip.Provider>
|
50 |
+
);
|
51 |
+
}
|
app/components/chat/ProgressCompilation.tsx
CHANGED
@@ -42,7 +42,6 @@ export default function ProgressCompilation({ data }: { data?: ProgressAnnotatio
|
|
42 |
'shadow-lg rounded-lg relative w-full max-w-chat mx-auto z-prompt',
|
43 |
'p-1',
|
44 |
)}
|
45 |
-
style={{ transform: 'translateY(1rem)' }}
|
46 |
>
|
47 |
<div
|
48 |
className={classNames(
|
|
|
42 |
'shadow-lg rounded-lg relative w-full max-w-chat mx-auto z-prompt',
|
43 |
'p-1',
|
44 |
)}
|
|
|
45 |
>
|
46 |
<div
|
47 |
className={classNames(
|
app/components/header/HeaderActionButtons.client.tsx
CHANGED
@@ -10,6 +10,8 @@ import { path } from '~/utils/path';
|
|
10 |
import { useEffect, useRef, useState } from 'react';
|
11 |
import type { ActionCallbackData } from '~/lib/runtime/message-parser';
|
12 |
import { chatId } from '~/lib/persistence/useChatHistory'; // Add this import
|
|
|
|
|
13 |
|
14 |
interface HeaderActionButtonsProps {}
|
15 |
|
@@ -25,6 +27,7 @@ export function HeaderActionButtons({}: HeaderActionButtonsProps) {
|
|
25 |
const canHideChat = showWorkbench || !showChat;
|
26 |
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
27 |
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
|
28 |
|
29 |
useEffect(() => {
|
30 |
function handleClickOutside(event: MouseEvent) {
|
@@ -41,7 +44,7 @@ export function HeaderActionButtons({}: HeaderActionButtonsProps) {
|
|
41 |
|
42 |
const handleDeploy = async () => {
|
43 |
if (!connection.user || !connection.token) {
|
44 |
-
toast.error('Please connect to Netlify first');
|
45 |
return;
|
46 |
}
|
47 |
|
@@ -206,7 +209,7 @@ export function HeaderActionButtons({}: HeaderActionButtonsProps) {
|
|
206 |
<div className="flex border border-bolt-elements-borderColor rounded-md overflow-hidden mr-2 text-sm">
|
207 |
<Button
|
208 |
active
|
209 |
-
disabled={isDeploying || !activePreview}
|
210 |
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
|
211 |
className="px-4 hover:bg-bolt-elements-item-backgroundActive flex items-center gap-2"
|
212 |
>
|
@@ -225,8 +228,8 @@ export function HeaderActionButtons({}: HeaderActionButtonsProps) {
|
|
225 |
handleDeploy();
|
226 |
setIsDropdownOpen(false);
|
227 |
}}
|
228 |
-
disabled={isDeploying || !activePreview}
|
229 |
-
className="flex items-center w-full px-4 py-2 text-sm text-bolt-elements-textPrimary hover:bg-bolt-elements-item-backgroundActive gap-2 rounded-md"
|
230 |
>
|
231 |
<img
|
232 |
className="w-5 h-5"
|
@@ -235,7 +238,8 @@ export function HeaderActionButtons({}: HeaderActionButtonsProps) {
|
|
235 |
crossOrigin="anonymous"
|
236 |
src="https://cdn.simpleicons.org/netlify"
|
237 |
/>
|
238 |
-
<span className="mx-auto">Deploy to Netlify</span>
|
|
|
239 |
</Button>
|
240 |
<Button
|
241 |
active={false}
|
|
|
10 |
import { useEffect, useRef, useState } from 'react';
|
11 |
import type { ActionCallbackData } from '~/lib/runtime/message-parser';
|
12 |
import { chatId } from '~/lib/persistence/useChatHistory'; // Add this import
|
13 |
+
import { streamingState } from '~/lib/stores/streaming';
|
14 |
+
import { NetlifyDeploymentLink } from '~/components/chat/NetlifyDeploymentLink.client';
|
15 |
|
16 |
interface HeaderActionButtonsProps {}
|
17 |
|
|
|
27 |
const canHideChat = showWorkbench || !showChat;
|
28 |
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
29 |
const dropdownRef = useRef<HTMLDivElement>(null);
|
30 |
+
const isStreaming = useStore(streamingState);
|
31 |
|
32 |
useEffect(() => {
|
33 |
function handleClickOutside(event: MouseEvent) {
|
|
|
44 |
|
45 |
const handleDeploy = async () => {
|
46 |
if (!connection.user || !connection.token) {
|
47 |
+
toast.error('Please connect to Netlify first in the settings tab!');
|
48 |
return;
|
49 |
}
|
50 |
|
|
|
209 |
<div className="flex border border-bolt-elements-borderColor rounded-md overflow-hidden mr-2 text-sm">
|
210 |
<Button
|
211 |
active
|
212 |
+
disabled={isDeploying || !activePreview || isStreaming}
|
213 |
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
|
214 |
className="px-4 hover:bg-bolt-elements-item-backgroundActive flex items-center gap-2"
|
215 |
>
|
|
|
228 |
handleDeploy();
|
229 |
setIsDropdownOpen(false);
|
230 |
}}
|
231 |
+
disabled={isDeploying || !activePreview || !connection.user}
|
232 |
+
className="flex items-center w-full px-4 py-2 text-sm text-bolt-elements-textPrimary hover:bg-bolt-elements-item-backgroundActive gap-2 rounded-md group relative"
|
233 |
>
|
234 |
<img
|
235 |
className="w-5 h-5"
|
|
|
238 |
crossOrigin="anonymous"
|
239 |
src="https://cdn.simpleicons.org/netlify"
|
240 |
/>
|
241 |
+
<span className="mx-auto">{!connection.user ? 'No Account Connected' : 'Deploy to Netlify'}</span>
|
242 |
+
{connection.user && <NetlifyDeploymentLink />}
|
243 |
</Button>
|
244 |
<Button
|
245 |
active={false}
|
app/lib/stores/netlify.ts
CHANGED
@@ -1,5 +1,7 @@
|
|
1 |
import { atom } from 'nanostores';
|
2 |
import type { NetlifyConnection } from '~/types/netlify';
|
|
|
|
|
3 |
|
4 |
// Initialize with stored connection or defaults
|
5 |
const storedConnection = typeof window !== 'undefined' ? localStorage.getItem('netlify_connection') : null;
|
@@ -25,3 +27,37 @@ export const updateNetlifyConnection = (updates: Partial<NetlifyConnection>) =>
|
|
25 |
localStorage.setItem('netlify_connection', JSON.stringify(newState));
|
26 |
}
|
27 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
import { atom } from 'nanostores';
|
2 |
import type { NetlifyConnection } from '~/types/netlify';
|
3 |
+
import { logStore } from './logs';
|
4 |
+
import { toast } from 'react-toastify';
|
5 |
|
6 |
// Initialize with stored connection or defaults
|
7 |
const storedConnection = typeof window !== 'undefined' ? localStorage.getItem('netlify_connection') : null;
|
|
|
27 |
localStorage.setItem('netlify_connection', JSON.stringify(newState));
|
28 |
}
|
29 |
};
|
30 |
+
|
31 |
+
export async function fetchNetlifyStats(token: string) {
|
32 |
+
try {
|
33 |
+
isFetchingStats.set(true);
|
34 |
+
|
35 |
+
const sitesResponse = await fetch('https://api.netlify.com/api/v1/sites', {
|
36 |
+
headers: {
|
37 |
+
Authorization: `Bearer ${token}`,
|
38 |
+
'Content-Type': 'application/json',
|
39 |
+
},
|
40 |
+
});
|
41 |
+
|
42 |
+
if (!sitesResponse.ok) {
|
43 |
+
throw new Error(`Failed to fetch sites: ${sitesResponse.status}`);
|
44 |
+
}
|
45 |
+
|
46 |
+
const sites = (await sitesResponse.json()) as any;
|
47 |
+
|
48 |
+
const currentState = netlifyConnection.get();
|
49 |
+
updateNetlifyConnection({
|
50 |
+
...currentState,
|
51 |
+
stats: {
|
52 |
+
sites,
|
53 |
+
totalSites: sites.length,
|
54 |
+
},
|
55 |
+
});
|
56 |
+
} catch (error) {
|
57 |
+
console.error('Netlify API Error:', error);
|
58 |
+
logStore.logError('Failed to fetch Netlify stats', { error });
|
59 |
+
toast.error('Failed to fetch Netlify statistics');
|
60 |
+
} finally {
|
61 |
+
isFetchingStats.set(false);
|
62 |
+
}
|
63 |
+
}
|
app/lib/stores/streaming.ts
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
import { atom } from 'nanostores';
|
2 |
+
|
3 |
+
export const streamingState = atom<boolean>(false);
|