3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ジュニちゃんが志を“つい語りたくなる体験コンテンツ”を作ってみた

Last updated at Posted at 2025-12-09

1. 初めに

この記事は、Japan AWS Jr. Champions Advent Calendar 2025 (シリーズ1)9日目の記事です。

AWS を使って「ブラウザから触って楽しめる体験型コンテンツ」をゼロから作り、実際に人に触ってもらった記録です。

想定の読者は、
「楽しいコンテンツを作ってみたいけど、どうしたらいいんだろう?って人」です。

技術的な細かいところより、「どうやって体験を設計し、どうやって AWS で支えたか」という視点で読んでいただければ幸いです。

目次

1.初めに
2.体験コンテンツのコンセプト
3.AWS構成
4.体験としての面白さ
5.実装のポイント
6.ふりかえり
7.まとめ

2. 体験コンテンツのコンセプト

したいことは3つです!

  • 話しにくいけど気になる「Jr. Championsがどんな志を持ってきたか」を対面で話しやすくしたい!
  • 物語の力を借りて、話し出すきっかけを作りたい!
  • 自分なりの言葉で書いたものに対して、自分らしさがわかる返答がほしい!

少しだけ背景

小見出し (3).png

Jr. Championsの内部コミュニティで「志LT」というイベントを1/31(土)にします。
このイベントでは「10年後、あなたはどんなChampion?」をテーマに、Jr.Championsのみなさんが将来について考えられる楽しい交流の場です!!

して欲しいコミュニケーションを体験してもらうために、初めてコンテンツ作りに挑戦してみました!!

現ジュニちゃんの人が近くにいたら、「参加しないの?」と声をかけてくれると嬉しいです!!

3. AWS構成

名称未設定のデザイン (14).png

今回はシンプルなWebページの構成になります。裏では、LLMの返答に合わせた遷移をしています。

4. 体験としての面白さ

これを入れると面白い体験になるんじゃないかなと思って作ってみました。

面白さ1:問いかけ

問いかけをされると、考えたくなりませんか?

この効果を活かして、「自分への興味」「相手への興味」を促そうと試みました。

スクリーンショット 2025-12-09 10.21.46.png

この問いかけから、体験がスタートします。
体験者にはなんとなくでもいいから、雰囲気が伝わるといいですね。

※面白さ3では、「相手への興味」を促すような問いかけもでてきます。

面白さ2:ロールプレイング

あなたは「探偵」です。

と言われると、ちょっと「捜査したいな」って思いませんか?

名称未設定のデザイン (15).png

「したいこと(夢)を思い出せない」=「落とし物をした」という設定にすることで

・夢を見つける探偵
・落とし物をした人

という2つの役割を導入してみました。

名称未設定のデザイン (16).png

対面コミュニケーションの場合、バッチリ言えるよ〜って人は、よく最初に話し始めてくれます。その良さと「探偵」という役割が重なり「話を積極的に聞きだしてくれる」と期待して作りました。

面白さ3:パーソナル診断

あなたは「S3」です。
と言われたら、「記憶力バッチリで過去の恩義も忘れない情の深い人」だと思いませんか?

(本音)う〜〜〜ん、思いませんね。。。

こんなふうに、「あなたは〇〇っぽいよね」ということから自分を深く知る体験はやってみたくなります。

名称未設定のデザイン (17).png

体験としては、
グループで話してもらった後、「ジュニちゃんでしたかったことは何?」の回答をもらいます。

名称未設定のデザイン (18).png

先ほどの回答を踏まえて、あなたにピッタリなサービスが診断されます。

「あなたの声を届ける」ボタンを押すと、志LTのアンケートリンクとなるので体験は終了です。

5. 実装のポイント

ポイント1:スマホが大前提

みんなで交流をしようで集まって「ここにアクセスしてね〜!」って言ったとき、最初に取り出すものは何でしょうか?

ほぼ100%スマホだと思います。
だからこそ、スマホを大前提において、楽しみやすいコンテンツを作りました。

@supports (min-height: 100svh) { body { min-height: 100svh; } }
@supports (min-height: 100dvh) { body { min-height: 100dvh; } }

HTML/CSSで作ってるときはPCなので、スマホで見られることを忘れがちです。
デプロイの都度、スマホで使い勝手が良いかを確かめました。

ポイント2:自分だけの体験に思える応答

みんなが似たような応答来てたら嫌じゃないですか?

MBTIも16種類あるから楽しいのであって、4種類だと楽しくありません。
AWSには大量にサービスがあるので、100種類超えでページを作ってみました。

名称未設定のデザイン (19).png

名称未設定のデザイン (21).png

ここで1つの疑問を解消しておきます。

Q. どうやって、ページ遷移をコントロールしているのか?
A. LLMの返答に応じて遷移を決めている。

みなさんの回答をプロンプトに入れて、LambdaでClaudeを呼び出しています。
その返答に応じて、ページ遷移先を決めています。

Lambdaの一部をご紹介です。

import boto3
from botocore.exceptions import BotoCoreError, ClientError

MODEL_ID = os.environ.get("BEDROCK_MODEL_ID", "apac.anthropic.claude-3-5-sonnet-20241022-v2:0")
DEFAULT_SERVICE = os.environ.get("DEFAULT_SERVICE", "aws")

SERVICE_ROUTES = {
    "aws": {"path": "./icon-aws.html"},
    "s3": {"path": "./icon-s3.html"},
    "costexplorer": {"path": "./icon-costexplorer.html"},
    "lambda": {"path": "./icon-lambda.html"},
    "ec2": {"path": "./icon-ec2.html"},
    "apigateway": {"path": "./icon-apigateway.html"},
    "dynamodb": {"path": "./icon-dynamodb.html"},
    "iam": {"path": "./icon-iam.html"},
    "vpc": {"path": "./icon-vpc.html"},
    "chime": {"path": "./icon-chime.html"},
    "bedrock": {"path": "./icon-bedrock.html"},
    "eventbridge": {"path": "./icon-eventbridge.html"},
    "cloudfront": {"path": "./icon-cloudfront.html"},
    ・・・たくさん登録している・・・
}

ALLOWED_SERVICES = [service for service in SERVICE_ROUTES if service != DEFAULT_SERVICE]
SERVICE_LIST_TEXT = ", ".join(ALLOWED_SERVICES)

EVALUATION_INSTRUCTIONS = f"""\
あなたはAWSサービス診断のためのアシスタントです。
参加者がやりたいこと・実現したいことの説明が与えられるので、次の候補から最も適切なものを1つだけ選び、日本語で短く理由も添えてください。
候補: {SERVICE_LIST_TEXT}

次の形式のJSONのみを出力してください。
{{"service": "<上記候補のいずれか>", "reason": "<日本語での簡潔な説明>"}}

余計な文章やキーを追加してはいけません。"""

try:
    bedrock_runtime = boto3.client("bedrock-runtime")
except Exception as error:  # pragma: no cover - safe guard
    logger.warning("Failed to initialise Bedrock client: %s", error)
    bedrock_runtime = None


def _response(status: int, body: dict[str, str]) -> dict:
    return {
        "statusCode": status,
        "headers": {
            "Content-Type": "application/json",
            "Access-Control-Allow-Origin": "*",
            "Access-Control-Allow-Headers": "Content-Type",
        },
        "body": json.dumps(body, ensure_ascii=False),
    }


def _classify_service(vision: str) -> tuple[str, str]:
    if not bedrock_runtime or not MODEL_ID:
        logger.warning("Bedrock runtime unavailable. Falling back to default service.")
        return DEFAULT_SERVICE, "生成AIが利用できなかったため、汎用サービスを提案します。"

    body = {
        "messages": [
            {
                "role": "user",
                "content": [
                    {
                        "type": "text",
                        "text": vision,
                    }
                ],
            }
        ],
        "system": EVALUATION_INSTRUCTIONS,
        "max_tokens": 300,
        "temperature": 0,
        "anthropic_version": "bedrock-2023-05-31",
    }

    try:
        response = bedrock_runtime.invoke_model(
            modelId=MODEL_ID,
            body=json.dumps(body),
        )
    except (ClientError, BotoCoreError, ValueError) as error:
        logger.exception("Failed to call Bedrock model: %s", error)
        return DEFAULT_SERVICE, "診断エンジンの呼び出しに失敗したため、汎用サービスを提案します。"

    try:
        payload = json.loads(response["body"].read())
        text_chunks = [block.get("text", "") for block in payload.get("content", []) if block.get("type") == "text"]
        combined = "".join(text_chunks).strip()
        classification = json.loads(combined)
        candidate = str(classification.get("service", "")).strip().lower()
        reason = str(classification.get("reason", "")).strip()
    except (KeyError, AttributeError, json.JSONDecodeError, ValueError) as error:
        logger.exception("Failed to parse Bedrock response: %s", error)
        return DEFAULT_SERVICE, "診断結果の解析に失敗したため、汎用サービスを提案します。"

    if candidate not in ALLOWED_SERVICES:
        logger.warning("Bedrock suggested unsupported service '%s'. Falling back.", candidate)
        unsupported = candidate or "empty"
        return DEFAULT_SERVICE, f"未対応のサービス候補({unsupported})が返されたため、汎用サービスを提案します。"

    return candidate, reason or "診断結果を生成AIが提案しました。"



def handler(event, _context):
    try:
        payload = json.loads(event.get("body") or "{}")
    except json.JSONDecodeError:
        return _response(400, {"message": "Invalid JSON payload."})

    vision = (payload.get("vision") or "").strip()
    dream = (payload.get("dream") or "").strip()
    if not vision:
        return _response(400, {"message": "vision is required."})

    # Bedrock によるサービス診断のみ実施
    service, reason = _classify_service(vision)

    redirect_path = SERVICE_ROUTES.get(service, SERVICE_ROUTES[DEFAULT_SERVICE])["path"]

    return _response(
        201,
        {
            "message": "Saved",
            "service": service,
            "reason": reason,
            "redirect": redirect_path,
        },
    )

ポイント3:エラーにも遊び心を

エラー画面って悲しくないですか?

エラー画面に行った時の絶望感。「わくわくを返せよ」という非難。わかります。
めっちゃ悲しいですよね。しかし、開発者からしたら、生成AIを使っているのでエラーとは隣り合わせです。

「エラーが起きないように」と対策を重ねますが、出るものは出ます。
今回は、エラーすらも楽しませようと発想を転換してみました。

名称未設定のデザイン (22).png

エラーでこの画面が出ても、悲しくありません。むしろ、超レアで嬉しいまであるかもしれません。
※体験した人からも、「エラーに AWS 使うの面白い」となって許してくれました。

確率だけでみたら、エラー画面はSSレアです。
エラーって言葉ではなく、「SSレア」と思うことで、エンジニアの人たちの心も平和になるかもしれません。

6. ふりかえり

この体験コンテンツをJr. Champions Meetup後にある交流会で使ってもらました。

期待とずれたところ

・「したいことがない」といって考えをやめちゃう人もいた。
  →ふかぼってみると、したいことはあったけど「自信」がないだけだった。問いかけまでの導入が足りなかっったかもしれない。

・どこで会話していいかわからなかった
  →スマホと対面を行き来するのは難しい。

・Bedrockの呼び出しでエラーが発生していた
  →みんなが一気にアクセスしたからかも?

期待通りだったところ

・問いがストレートだけど、みんなうまく思いを話してくれた。
・診断結果で自分は〇〇が出たって盛り上がった
・この問いをきっかけに、お互いのことを知るいい機会になった

ジュニちゃんの傾向

せっかく、パーソナル診断をしたのでジュニちゃんの傾向を分析してみます。

25名の現ジュニちゃんにご回答いただきました。

newplot.png

100種類超で用意したのに、こんなにも結果が固まるなんて、、、
驚きですね!

では、ジュニちゃんにはどんな志を持った人が多いかを詳しく分析してみます。

名称未設定のデザイン (25).png

1位: Amazon Chime

「コミュニティを活用して学生支援をしたい」
「同世代の社内外に限らない交流の場を設けること」
「Jr. Championsだからこそ巡り合う機会を大事に積極的に参加し、自身もその場を外部へ提供していきたい」

など、Jr.Champions での「交流」や「コミュニティ」を大事にしている傾向があります。
Jr.Championsを交流の起点として日々活動されているみなさんが多いというイメージと合致します!!

2位: AWS Marketplace

「ジャニちゃんの活動を通してAWSの知見やコミュニティを広げたい。それを社内に還元して、自社のAWS学習に対するモチベーションを上げたい。」
「コミュニティに参加し、社外とのネットワークも広げながら、社内のAWSに関するレベルを底上げしたい」
「面白いことしたい!社外の色々な方のナレッジや知見、取り組みを知り、自分1人ではできなかったことをみんなでワイワイやりたい!」

「人とのつながり・横のつながり」いわゆる「ネットワーク」を重視する傾向に見えます。
繋がりと一緒に、ナリッジをシェアしていきたいという、まさに Public Contributionですね!

◇Market Placeの解釈
Marketplace は、「提供者(パートナー / ベンダー)」と「利用者(自社 / お客様)」をつなぐ「場」です。つまり、「外部のものを自社にどう適用するかを一緒に考える」という「内と外をつなぐ・広げる活動」と解釈をすれば、近しいものを感じられるかもしれません。

名称未設定のデザイン (26).png

3位:Amazon GameLift

「仲間と一緒に企業し、完全没入型仮想現実の技術を利用した、全世界の人間の機会が均等な社会を作る」
「同年代の仲間と狂う」

没入型といえばVRですね。そして、ゲームは熱狂します。
そんな「熱狂できるような志」を持った人がジュニちゃんにもいます!!

3位:AWS Organizations

会社を立ち上げたい
会社同士のコラボをしたい

「会社」という大きな組織を動かしたいという傾向がありそうです。
ジュニちゃんは、組織を動かすほどの影響力があります。
その影響力を良い形で用いて、人材育成や技術推進をしている人が多いイメージがありますね!!

3位:Amazon EventBridge

「ハッカソンや企画の運営を通して関係を深め、ジュニチャンコミュニティの枠を超える!」
「スキルアップをして熱量ある仲間と機会を活かしたイベントを開催しつつ、未来の創業メンバーを見つける」

「参加者」ではなく「主催・運営」に強い興味がある傾向がみられます。
Amazonの「Builderたれ」を体現している、まさに、人間版EventBridgeですね!

7. まとめ

今回の取り組みは、「ジュニちゃんのみんなが楽しく志を共有してほしい」という思いで、初めて体験コンテンツを作ってみました。

・どんな体験をしてほしいかを言語化する
・それを支える最小限の AWS アーキテクチャを組む
・実際にフィードバックを得る

というサイクルを回すことで、ゼロからでも十分に「ブラウザから触って楽しめる体験型コンテンツ」が作れると実感しました。

この記事が、「自分もなにか体験コンテンツを作ってみようかな?」と一歩踏み出すきっかけになればうれしいです。

また、
現役の Jr.Champions のみなさんは、「その志が外から見たら眩しいものである」と自信を持ってほしいですし、
ジュニちゃんを目指す人には、「志高く研鑽している最高の人たち」が集まったコミュニティに入信して、あなたも輝いてほしいと願っています!!

みなさんも よき志を!

宣伝

小見出し (3).jpg

あなたも唯一無二の志があると自信が持てるようになるイベントです!!
現役 Jr. Championsのみなさんは、ぜひご参加お待ちしています!
周りに 現役 Jr. Champions がいる方はぜひ参加を進めてくださると嬉しいです!

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?