Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show YT's AI chat summary in a 'pinned'-style box if it is present #148

Merged
merged 14 commits into from
Dec 16, 2024
Merged
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
71 changes: 71 additions & 0 deletions src/components/ChatSummary.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<script lang="ts">
import { slide, fade } from 'svelte/transition';
import MessageRun from './MessageRuns.svelte';
import Tooltip from './common/Tooltip.svelte';
import Icon from 'smelte/src/components/Icon';
import { Theme } from '../ts/chat-constants';
import { createEventDispatcher } from 'svelte';

export let summary: Ytc.ParsedSummary;

let dismissed = false;
let shorten = false;
const classes = 'rounded inline-flex flex-col overflow-visible ' +
'bg-secondary-900 p-2 w-full text-white z-10 shadow';

const onShorten = () => { shorten = !shorten; };

$: if (summary) {
dismissed = false;
shorten = false;
}

const dispatch = createEventDispatcher();
$: dismissed, shorten, dispatch('resize');
</script>

{#if !dismissed}
<div
class={classes}
transition:fade={{ duration: 250 }}
>
<div class="flex flex-row items-center cursor-pointer" on:click={onShorten}>
<div class="font-medium tracking-wide text-white flex-1">
<span class="mr-1 inline-block" style="transform: translateY(3px);">
<Icon small>
{#if shorten}
expand_more
{:else}
expand_less
{/if}
</Icon>
</span>
{#each summary.item.header as run}
{#if run.type === 'text'}
<span class="align-middle">{run.text}</span>
{/if}
{/each}
</div>
<div class="flex-none self-end" style="transform: translateY(3px);">
<Tooltip offsetY={0} small>
<Icon
slot="activator"
class="cursor-pointer text-lg"
on:click={() => { dismissed = true; }}
>
close
</Icon>
Dismiss
</Tooltip>
</div>
</div>
{#if !shorten && !dismissed}
<div class="mt-1 whitespace-pre-line" transition:slide|local={{ duration: 300 }}>
<MessageRun runs={summary.item.subheader} deleted forceDark forceTLColor={Theme.DARK}/>
</div>
<div class="mt-1 whitespace-pre-line" transition:slide|local={{ duration: 300 }}>
<MessageRun runs={summary.item.message} forceDark forceTLColor={Theme.DARK}/>
</div>
{/if}
</div>
{/if}
21 changes: 17 additions & 4 deletions src/components/Hyperchat.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import WelcomeMessage from './WelcomeMessage.svelte';
import Message from './Message.svelte';
import PinnedMessage from './PinnedMessage.svelte';
import ChatSummary from './ChatSummary.svelte';
import PaidMessage from './PaidMessage.svelte';
import MembershipItem from './MembershipItem.svelte';
import ReportBanDialog from './ReportBanDialog.svelte';
Expand All @@ -28,6 +29,7 @@
showUsernames,
showTimestamps,
showUserBadges,
showChatSummary,
refreshScroll,
emojiRenderMode,
useSystemEmojis,
Expand All @@ -53,6 +55,7 @@
let messageActions: (Chat.MessageAction | Welcome)[] = [];
const messageKeys = new Set<string>();
let pinned: Ytc.ParsedPinned | null;
let summary: Ytc.ParsedSummary | null;
let div: HTMLElement;
let isAtBottom = true;
let truncateInterval: number;
Expand Down Expand Up @@ -183,6 +186,9 @@
case 'delete':
onDelete(action.deletion);
break;
case 'summary':
summary = action;
break;
case 'pin':
pinned = action;
break;
Expand Down Expand Up @@ -393,11 +399,18 @@
</div>
{/each}
</div>
{#if pinned}
{#if (summary && $showChatSummary) || pinned}
<div class="absolute top-0 w-full" bind:this={topBar}>
<div class="mx-1.5 mt-1.5">
<PinnedMessage pinned={pinned} on:resize={topBarResized} />
</div>
{#if summary && $showChatSummary}
<div class="mx-1.5 mt-1.5">
<ChatSummary summary={summary} on:resize={topBarResized} />
</div>
{/if}
{#if pinned}
<div class="mx-1.5 mt-1.5">
<PinnedMessage pinned={pinned} on:resize={topBarResized} />
</div>
{/if}
</div>
{/if}
{#if !isAtBottom}
Expand Down
4 changes: 3 additions & 1 deletion src/components/settings/InterfaceSettings.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
useSystemEmojis,
isDark,
enableStickySuperchatBar,
enableHighlightedMentions
enableHighlightedMentions,
showChatSummary
} from '../../ts/storage';
import { themeItems, emojiRenderItems } from '../../ts/chat-constants';
import Card from '../common/Card.svelte';
Expand Down Expand Up @@ -59,6 +60,7 @@
<Checkbox name="Show timestamps" store={showTimestamps} />
<Checkbox name="Show usernames" store={showUsernames} />
<Checkbox name="Show user badges" store={showUserBadges} />
<Checkbox name="Show experimental chat summaries by YouTube" store={showChatSummary} />
<Checkbox name="Highlight mentions" store={enableHighlightedMentions} />
</Card>

Expand Down
52 changes: 50 additions & 2 deletions src/ts/chat-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,51 @@ const parseMessageRuns = (runs?: Ytc.MessageRun[]): Ytc.ParsedRun[] => {
return parsedRuns;
};

// takes an array of runs, finds newline-only runs, and splits the array by them, up to maxSplit times
// final output will have maximum length of maxSplit + 1
// maxSplit = -1 will have no limit for splits
const splitRunsByNewline = (runs: Ytc.ParsedRun[], maxSplit: number = -1): Ytc.ParsedRun[][] =>
runs.reduce((acc: Ytc.ParsedRun[][], run: Ytc.ParsedRun) => {
if (run.type === 'text' && run.text === '\n' && (maxSplit == -1 || acc.length <= maxSplit)) {
acc.push([]);
} else {
acc[acc.length - 1].push(run);
}
return acc;
}, [[]]);

const parseChatSummary = (renderer: Ytc.AddChatItem, isEphemeral: boolean, bannerTimeoutMs: number): Ytc.ParsedSummary | undefined => {
if (!renderer.liveChatBannerChatSummaryRenderer) {
return;
}
const baseRenderer = renderer.liveChatBannerChatSummaryRenderer!;
const runs = parseMessageRuns(renderer.liveChatBannerChatSummaryRenderer?.chatSummary.runs);
const splitRuns = splitRunsByNewline(runs, 2);
if (splitRuns.length < 3) {
// YT probably changed the format, refuse to do anything to avoid breaking
return;
}
const subheader = splitRuns[1].map(run => {
if (run.type === 'text') {
// turn subheader into a link to YT's support page detailing the AI summary feature
return { type: 'link', text: run.text, url: 'https://support.google.com/youtube/thread/18138167?msgid=284199217' } as Ytc.ParsedLinkRun;
} else {
return run;
}
});
const item: Ytc.ParsedSummary = {
type: 'summary',
item: {
header: splitRuns[0],
subheader: subheader,
message: splitRuns[2],
},
id: baseRenderer.liveChatSummaryId,
showtime: isEphemeral ? (bannerTimeoutMs / 1000) : 0,
};
return item;
}

const parseAddChatItemAction = (action: Ytc.AddChatItemAction, isReplay = false, liveTimeoutOrReplayMs = 0): Ytc.ParsedMessage | undefined => {
const actionItem = action.item;
const renderer = actionItem.liveChatTextMessageRenderer ??
Expand Down Expand Up @@ -183,8 +228,11 @@ const parseMessageDeletedAction = (action: Ytc.MessageDeletedAction): Ytc.Parsed
};
};

const parsePinnedMessageAction = (action: Ytc.AddPinnedAction): Ytc.ParsedPinned | undefined => {
const parseBannerAction = (action: Ytc.AddPinnedAction): Ytc.ParsedPinned | Ytc.ParsedSummary | undefined => {
const baseRenderer = action.bannerRenderer.liveChatBannerRenderer;
if (baseRenderer.contents.liveChatBannerChatSummaryRenderer) {
return parseChatSummary(baseRenderer.contents, action.bannerProperties?.isEphemeral ?? false, action.bannerProperties?.bannerTimeoutMs ?? 0);
}
const parsedContents = parseAddChatItemAction(
{ item: baseRenderer.contents }, true
);
Expand Down Expand Up @@ -229,7 +277,7 @@ const processCommonAction = (
if (action.addChatItemAction) {
return parseAddChatItemAction(action.addChatItemAction, isReplay, liveTimeoutOrReplayMs);
} else if (action.addBannerToLiveChatCommand) {
return parsePinnedMessageAction(action.addBannerToLiveChatCommand);
return parseBannerAction(action.addBannerToLiveChatCommand);
} else if (action.removeBannerForLiveChatCommand) {
return { type: 'unpin' } as const;
} else if (action.addLiveChatTickerItemAction) {
Expand Down
1 change: 1 addition & 0 deletions src/ts/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export const showProfileIcons = stores.addSyncStore('hc.messages.showProfileIcon
export const showUsernames = stores.addSyncStore('hc.messages.showUsernames', true);
export const showTimestamps = stores.addSyncStore('hc.messages.showTimestamps', false);
export const showUserBadges = stores.addSyncStore('hc.messages.showUserBadges', true);
export const showChatSummary = stores.addSyncStore('hc.messages.showChatSummary', true);
export const lastClosedVersion = stores.addSyncStore('hc.lastClosedVersion', '');
export const showOnlyMemberChat = stores.addSyncStore('hc.showOnlyMemberChat', false);
export const emojiRenderMode = stores.addSyncStore('hc.emojiRenderMode', YoutubeEmojiRenderMode.SHOW_ALL);
Expand Down
28 changes: 27 additions & 1 deletion src/ts/typings/ytc.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ declare namespace Ytc {
};
};
};
bannerProperties?: {
isEphemeral: boolean;
bannerTimeoutMs: number;
}
}

interface AddTickerAction {
Expand Down Expand Up @@ -227,6 +231,15 @@ declare namespace Ytc {
};
}

interface ChatSummaryRenderer {
liveChatSummaryId: string;
chatSummary: RunsObj;
icon?: {
/** Unlocalized string */
iconType: string;
};
}

interface PlaceholderRenderer { // No idea what the purpose of this is
id: string;
timestampUsec: IntString;
Expand All @@ -248,6 +261,8 @@ declare namespace Ytc {
liveChatSponsorshipsGiftPurchaseAnnouncementRenderer?: MembershipGiftPurchaseRenderer;
/** Membership gift redemption */
liveChatSponsorshipsGiftRedemptionAnnouncementRenderer?: TextMessageRenderer;
/** AI Chat Summary */
liveChatBannerChatSummaryRenderer?: ChatSummaryRenderer;
/** ??? */
liveChatPlaceholderItemRenderer?: PlaceholderRenderer;
}
Expand Down Expand Up @@ -365,13 +380,24 @@ declare namespace Ytc {
};
}

interface ParsedSummary {
type: 'summary';
item: {
header: ParsedRun[];
subheader: ParsedRun[];
message: ParsedRun[];
};
id: string;
showtime: number;
}

interface ParsedTicker extends ParsedMessage {
type: 'ticker';
tickerDuration: number;
detailText?: string;
}

type ParsedMisc = ParsedPinned | { type: 'unpin'};
type ParsedMisc = ParsedPinned | ParsedSummary | { type: 'unpin' };

type ParsedTimedItem = ParsedMessage | ParsedTicker;

Expand Down
Loading