Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/assets/icons/chatgpt.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/assets/icons/claude.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions src/assets/icons/external-link.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/assets/icons/gemini.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 19 additions & 5 deletions src/components/Button.astro
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ type SharedProps = {
disabled?: boolean;
};

// Extend the native button attributes, or the anchor ones when an href is given
// Extend the native button attributes, the anchor ones when an href is given,
// or the summary ones when the button is a `<details>` disclosure trigger
type Props =
| (HTMLAttributes<'button'> & SharedProps & { href?: never })
| (HTMLAttributes<'a'> & SharedProps & { href: string | URL });
| (HTMLAttributes<'button'> & SharedProps & { href?: never; as?: 'button' })
| (HTMLAttributes<'a'> & SharedProps & { href: string | URL; as?: never })
| (HTMLAttributes<'summary'> & SharedProps & { href?: never; as: 'summary' });

const {
label,
Expand All @@ -23,6 +25,7 @@ const {
importance = 'default',
disabled = false,
href,
as = 'button',
type = 'button',
class: className,
...rest
Expand All @@ -41,7 +44,9 @@ const tagSpecificProps = href
'aria-disabled': disabled ? 'true' : undefined,
tabindex: disabled ? -1 : undefined,
}
: { type, disabled };
: as === 'summary'
? { 'aria-disabled': disabled ? 'true' : undefined }
: { type, disabled };

const iconDetails = (name: string | undefined) => {
if (!name) return null;
Expand All @@ -55,7 +60,7 @@ const endIcon = iconDetails(iconEnd);
const StartIconTag = startIcon?.tag;
const EndIconTag = endIcon?.tag;

const Tag = href ? 'a' : 'button';
const Tag = href ? 'a' : as;
---

<Tag class={classes} {...tagSpecificProps} {...rest}>
Expand Down Expand Up @@ -106,6 +111,15 @@ const Tag = href ? 'a' : 'button';
--btn-padding-inline: var(--space4);
}

summary.btn {
list-style: none;
user-select: none;
}

summary.btn::-webkit-details-marker {
display: none;
}

.btn__label {
padding-inline: var(--space4);
}
Expand Down
87 changes: 0 additions & 87 deletions src/components/CopyMarkdown.astro

This file was deleted.

224 changes: 224 additions & 0 deletions src/components/OpenInLlm.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
---
import { SITE } from '@config';
import { Lang, Translations } from '@util/Languages';
import { getEligibleSlugs } from '@util/mdxContent';
import Button from './Button.astro';

type Props = {
lang: string;
};
const { lang } = Astro.props satisfies Props;

const _ = Lang(lang);

const docsPath = Astro.url.pathname.replace(/\/$/, '');
const subfolderPrefix = SITE.subfolder.replace(/\/$/, '') + '/';
const docsSlug = docsPath.startsWith(subfolderPrefix)
? docsPath.slice(subfolderPrefix.length)
: null;
const eligibleSlugs = getEligibleSlugs();
const pageMdUrl =
docsSlug && eligibleSlugs.has(docsSlug) ? docsPath + '.md' : null;

// The assistant fetches the page itself, so the prompt carries an absolute URL
// rather than the site-relative one the rest of the site links with.
const prompt = pageMdUrl
? _(Translations.octopus_open_in_llm.prompt).replace(
'{url}',
new URL(pageMdUrl, SITE.url).href
)
: '';

// gemini.google.com ignores prompt parameters, so "Open in Gemini" targets AI
// Studio, the Gemini surface that accepts one.
const assistants = [
{
id: 'claude',
label: _(Translations.octopus_open_in_llm.open_in_claude),
href: 'https://claude.ai/new?q=' + encodeURIComponent(prompt),
},
{
id: 'chatgpt',
label: _(Translations.octopus_open_in_llm.open_in_chatgpt),
href: 'https://chatgpt.com/?hints=search&q=' + encodeURIComponent(prompt),
},
{
id: 'gemini',
label: _(Translations.octopus_open_in_llm.open_in_gemini),
href:
'https://aistudio.google.com/prompts/new_chat?prompt=' +
encodeURIComponent(prompt),
},
];

const [primary] = assistants;
---

{
pageMdUrl && (
<div class="octo-llm">
<Button
class="octo-llm__primary"
href={primary.href}
icon={`octo-llm__icon octo-llm__icon--${primary.id}`}
label={primary.label}
size="small"
target="_blank"
rel="noopener"
/>
<details class="octo-llm__menu" data-llm-menu>
<Button
as="summary"
class="octo-llm__toggle"
icon="fa-solid fa-caret-down"
size="small"
aria-label={_(Translations.octopus_open_in_llm.more_assistants)}
data-llm-trigger
/>
<ul class="octo-llm__options">
{assistants.map((assistant) => (
<li>
<a
class="octo-llm__option"
href={assistant.href}
target="_blank"
rel="noopener"
>
<span
class={`octo-llm__icon octo-llm__icon--${assistant.id}`}
aria-hidden="true"
/>
<span class="octo-llm__option-label">{assistant.label}</span>
<span class="octo-llm__external" aria-hidden="true" />
</a>
</li>
))}
</ul>
</details>
</div>
)
}

<style>
.octo-llm {
display: inline-flex;
align-items: stretch;
}

/* Both halves keep the border `.btn` gives them so their focus rings stay
unclipped; the negative margin collapses the touching pair into the single
divider the design asks for. */
.octo-llm__menu {
position: relative;
margin-inline-start: calc(var(--borderWidth1) * -1);
}

/* `:global()` crosses into Button's scope; the `.octo-llm` prefix stays scoped */
.octo-llm :global(.octo-llm__primary) {
border-start-end-radius: 0;
border-end-end-radius: 0;
}

.octo-llm :global(.octo-llm__toggle) {
height: 100%;
border-start-start-radius: 0;
border-end-start-radius: 0;
}

/* Raise whichever half is interactive so its border and outline sit above the
neighbour they overlap. */
.octo-llm :global(.octo-llm__primary:hover),
.octo-llm :global(.octo-llm__primary:focus-visible),
.octo-llm__menu:has(:hover),
.octo-llm__menu:has(:focus-visible),
.octo-llm__menu[open] {
z-index: 1;
}

.octo-llm__menu[open] > :global(.octo-llm__toggle) {
background: var(--colorBackgroundPrimaryPressed);
}

.octo-llm__options {
position: absolute;
z-index: 10;
inset-block-start: calc(100% + var(--space4));
inset-inline-start: 0;
box-sizing: border-box;
width: 16rem;
margin: 0;
padding: var(--space8) 0;
list-style: none;
background: var(--colorMenuListBackgroundDefault);
border-radius: var(--borderRadiusSmall);
box-shadow: var(--shadowMedium);
}

/* The list is wider than the button it hangs from, so below this width it has
to grow from the other edge to stay inside the content column. */
@media (max-width: 30rem) {
.octo-llm__options {
inset-inline-start: auto;
inset-inline-end: 0;
}
}

.octo-llm__option {
display: flex;
align-items: center;
gap: var(--space4);
min-height: 3rem;
padding: var(--space6) var(--space16);
color: var(--colorTextLinkDefault);
font: var(--textBodyRegularLarge);
}

.octo-llm__option:hover,
.octo-llm__option:focus-visible {
background: var(--colorMenuListBackgroundHover);
}

.octo-llm :global(.octo-llm__icon),
.octo-llm :global(.octo-llm__external) {
display: inline-block;
flex-shrink: 0;
background-color: currentColor;
}

/* The exported glyphs sit inside a box 25% wider than themselves, so the mask
is sized rather than left to fill. */
.octo-llm :global(.octo-llm__primary .octo-llm__icon) {
--octo-llm-glyph-size: 1rem;
}

.octo-llm__option .octo-llm__icon,
.octo-llm__external {
--octo-llm-glyph-size: 0.8rem;
width: 1rem;
height: 1rem;
}

.octo-llm :global(.octo-llm__icon--claude) {
mask: url('../assets/icons/claude.svg') center / var(--octo-llm-glyph-size)
no-repeat;
}

.octo-llm :global(.octo-llm__icon--chatgpt) {
mask: url('../assets/icons/chatgpt.svg') center / var(--octo-llm-glyph-size)
no-repeat;
}

.octo-llm :global(.octo-llm__icon--gemini) {
mask: url('../assets/icons/gemini.svg') center / var(--octo-llm-glyph-size)
no-repeat;
}

.octo-llm__external {
mask: url('../assets/icons/external-link.svg') center /
var(--octo-llm-glyph-size) no-repeat;
}

.octo-llm__option-label {
text-decoration: underline;
}
</style>
Loading